aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKarsten Heimrich <karsten.heimrich@qt.io>2025-10-28 15:12:29 +0100
committerMiguel Costa <miguel.costa@qt.io>2025-11-13 12:39:01 +0000
commitedb46ca50cca2d063b9bc4a6c62f3dbc7167192a (patch)
tree4d0845bee751b48671118574fd1489c098f0c446
parent376e975d35f2b07533dc70c306f05125159c2adc (diff)
Complement "Add DependsOn condition to generation rules" with auto-tests
* Adjust test code generator to accept additional rules * Add tests showcasing the DependsOn conditon Change-Id: I7e403f2bffbdcb6765ae4d8dfe8ea7f12b8f4d18 Reviewed-by: Miguel Costa <miguel.costa@qt.io> Reviewed-by: Karsten Heimrich <karsten.heimrich@qt.io>
-rw-r--r--tests/Test_Qt.DotNet.Generator/Support/TestCodeGenerator.cs6
-rw-r--r--tests/Test_Qt.DotNet.Generator/Test_RuleDependsOn.cs201
2 files changed, 207 insertions, 0 deletions
diff --git a/tests/Test_Qt.DotNet.Generator/Support/TestCodeGenerator.cs b/tests/Test_Qt.DotNet.Generator/Support/TestCodeGenerator.cs
index 8d46c18..e655ea8 100644
--- a/tests/Test_Qt.DotNet.Generator/Support/TestCodeGenerator.cs
+++ b/tests/Test_Qt.DotNet.Generator/Support/TestCodeGenerator.cs
@@ -45,11 +45,13 @@ namespace Test_Qt.DotNet.Generator.Support
/// <param name="sourceRefs">List of reference assemblies</param>
/// <param name="extraRefs">Additional directories to search for assembly references.</param>
/// <param name="referencesWithAliases">Aliased references (extern alias support).</param>
+ /// <param name="extraRules">Array of custom none build-in rules to apply.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Generated code and metadata.</returns>
public static async Task<Result> GenerateAsync(string[] sources,
Assembly[] sourceRefs = null, string[] extraRefs = null,
List<(string Alias, string Path)> referencesWithAliases = null,
+ Type[] extraRules = null,
CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(sources.ToString(), nameof(sources));
@@ -137,6 +139,10 @@ namespace Test_Qt.DotNet.Generator.Support
foreach (var t in typeof(GenerateIndexer).Assembly.ExportedTypes)
_ = t.TryRegisterAsRule() || t.TryRegisterAsMetaFunction();
+ // Register additional none build-in rules
+ foreach (var t in extraRules ?? [])
+ _ = t.TryRegisterAsRule();
+
// 4. Build dependency graph and run rules
await DependencyGraph.CreateAsync(metadataLoadContext, sourceAssembly,
Array.Empty<Type>());
diff --git a/tests/Test_Qt.DotNet.Generator/Test_RuleDependsOn.cs b/tests/Test_Qt.DotNet.Generator/Test_RuleDependsOn.cs
new file mode 100644
index 0000000..5ec3d7b
--- /dev/null
+++ b/tests/Test_Qt.DotNet.Generator/Test_RuleDependsOn.cs
@@ -0,0 +1,201 @@
+/***************************************************************************************************
+ Copyright (C) 2025 The Qt Company Ltd.
+ SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+***************************************************************************************************/
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Test_Qt.DotNet.Generator
+{
+ using Qt.DotNet.CodeGeneration;
+ using Support;
+
+ [TestClass]
+ public class Test_RuleDependsOn
+ {
+ private static volatile bool IsRuleAFinished;
+ private static readonly ConcurrentQueue<string> Log = new();
+
+ private static void ResetTestState()
+ {
+ while (Log.TryDequeue(out _))
+ { }
+ IsRuleAFinished = false;
+ Rules.Reset();
+ FilePlaceholder.All.Reset();
+ }
+
+ private static MemberInfo FindTypeByFullName(string fullName)
+ {
+ var graph = Rules.SourceGraph;
+ var b = graph?
+ .Where(kv => kv.Key is { } type && (type.FullName ?? type.Name) == "Dep.B")
+ .Select(kv => kv.Key)
+ .FirstOrDefault();
+ return b?.Assembly?.GetType(fullName);
+ }
+
+ private class RuleA_Succeeds : Rule
+ {
+ public override bool Matches(MemberInfo source)
+ => source is Type type && (type.FullName ?? type.Name) == "Dep.A";
+
+ public override Task<Result> ExecuteAsync(MemberInfo source)
+ {
+ Log.Enqueue("A:start");
+ IsRuleAFinished = true;
+ Log.Enqueue("A:end");
+ return Task.FromResult(Ok);
+ }
+ }
+
+ private class RuleA_Fails : Rule
+ {
+ public override bool Matches(MemberInfo source)
+ => source is Type type && (type.FullName ?? type.Name) == "Dep.A";
+
+ public override Task<Result> ExecuteAsync(MemberInfo source)
+ {
+ Log.Enqueue("A:fail");
+ return Task.FromResult(Error("A failed intentionally"));
+ }
+ }
+
+ private class RuleA_DependsOnB : Rule
+ {
+ public override bool Matches(MemberInfo source)
+ => source is Type type && (type.FullName ?? type.Name) == "Dep.A";
+
+ public override IEnumerable<MemberInfo> DependsOn
+ {
+ get {
+ var b = FindTypeByFullName("Dep.B");
+ return b is null ? Array.Empty<MemberInfo>() : new[] { b };
+ }
+ }
+
+ public override Task<Result> ExecuteAsync(MemberInfo source)
+ {
+ Log.Enqueue("A:start");
+ Log.Enqueue("A:end");
+ return Task.FromResult(Ok);
+ }
+ }
+
+ private class RuleB_DependsOnA : Rule
+ {
+ public override IEnumerable<MemberInfo> DependsOn
+ {
+ get {
+ var a = FindTypeByFullName("Dep.A");
+ return a is null ? Array.Empty<MemberInfo>() : new[] { a };
+ }
+ }
+
+ public override bool Matches(MemberInfo source)
+ => source is Type type && (type.FullName ?? type.Name) == "Dep.B";
+
+ public override Task<Result> ExecuteAsync(MemberInfo source)
+ {
+ Log.Enqueue("B:start");
+ if (!IsRuleAFinished)
+ return Task.FromResult(Error("B ran before A finished"));
+ Log.Enqueue("B:end");
+ return Task.FromResult(Ok);
+ }
+ }
+
+ private const string Source = """
+ namespace Dep {
+ public class A { public A() {} }
+ public class B { public B() {} }
+ }
+ """;
+
+ public TestContext TestContext { get; set; }
+
+ [TestMethod]
+ public async Task DependsOn_B_Runs_After_A()
+ {
+ ResetTestState();
+ await TestCodeGenerator.GenerateAsync([Source],
+ extraRules: [typeof(RuleA_Succeeds), typeof(RuleB_DependsOnA)],
+ ct: TestContext.CancellationTokenSource.Token);
+
+ var log = Log.ToArray();
+ var aEndIndex = Array.FindIndex(log, s => s == "A:end");
+ var bStartIndex = Array.FindIndex(log, s => s == "B:start");
+
+ Assert.IsTrue(aEndIndex >= 0, "A:end missing");
+ Assert.IsTrue(bStartIndex >= 0, "B:start missing");
+ Assert.IsTrue(aEndIndex < bStartIndex, "B must start after A:end (DependsOn not respected).");
+ Assert.IsFalse(log.Any(s => s.Contains("fail", StringComparison.OrdinalIgnoreCase)));
+ }
+
+ [TestMethod]
+ public async Task DependsOn_B_Is_Suppressed_When_A_Fails()
+ {
+ ResetTestState();
+ try {
+ _ = await TestCodeGenerator.GenerateAsync(
+ [Source],
+ extraRules: [typeof(RuleA_Fails), typeof(RuleB_DependsOnA)],
+ ct: TestContext.CancellationTokenSource.Token);
+ Assert.Fail("Generator should have failed when A failed.");
+ } catch (InvalidOperationException ex) {
+ Assert.Contains("failed", ex.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ var log = Log.ToArray();
+ Assert.IsTrue(log.Contains("A:fail"), "A:fail not logged");
+ Assert.IsFalse(log.Contains("B:start"), "B should not run when A fails.");
+ }
+
+ [TestMethod]
+ public async Task DependsOn_MissingDependency_Fails_B()
+ {
+ const string onlyB
+ = "namespace Dep { internal class A { } public class B { public B() {} } }";
+
+ ResetTestState();
+ try {
+ _ = await TestCodeGenerator.GenerateAsync([onlyB],
+ extraRules: [typeof(RuleB_DependsOnA)],
+ ct: TestContext.CancellationTokenSource.Token);
+ Assert.Fail("Generator should have failed (missing dependency A).");
+ } catch (InvalidOperationException ex) {
+ Assert.Contains("failed", ex.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ var log = Log.ToArray();
+ Assert.IsFalse(log.Contains("B:start"), "B should not run without A.");
+ }
+
+ [TestMethod]
+ public async Task DependsOn_Cycle_ShouldBeDetected_AndFailFast()
+ {
+ ResetTestState();
+
+ var generatorTaskObject = TestCodeGenerator.GenerateAsync([Source],
+ extraRules: [typeof(RuleA_DependsOnB), typeof(RuleB_DependsOnA)],
+ ct: TestContext.CancellationTokenSource.Token);
+
+ var finishedTaskObject = await Task.WhenAny(generatorTaskObject, Task.Delay(1000));
+ Assert.AreEqual(generatorTaskObject, finishedTaskObject,
+ "Cycle NOT detected: Generation did not complete fast (it is hanging).");
+
+ try {
+ await generatorTaskObject;
+ Assert.Fail("Generation completed without error despite a dependency cycle.");
+ } catch (InvalidOperationException ex) {
+ Assert.Contains("fail", ex.Message, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+ }
+}