From a9de420da150666c229a87ec6233f75b9b53fc4e Mon Sep 17 00:00:00 2001 From: James Alickolli Date: Tue, 12 May 2026 20:45:22 +1000 Subject: [PATCH 01/14] Created FSharp Testing project --- Directory.Packages.props | 1 + ModularPipelines.sln | 19 +++++- .../Data/BashTest.sh | 2 + .../Data/Foo.txt | 1 + .../Data/LocalWebpage.html | 10 +++ .../Nest1/Nest2/Nest3/Nest4/Nest5/Blah.txt | 1 + .../Data/Zip/Lorem.txt | 49 ++++++++++++++ .../GlobalDummyModule.fs | 9 +++ .../GlobalTestSetup.fs | 11 ++++ .../ModularPipelines.UnitTests.FSharp.fsproj | 65 +++++++++++++++++++ .../ScaleTests.fs | 17 +++++ .../TestConstants.fs | 11 ++++ 12 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 test/ModularPipelines.UnitTests.FSharp/Data/BashTest.sh create mode 100644 test/ModularPipelines.UnitTests.FSharp/Data/Foo.txt create mode 100644 test/ModularPipelines.UnitTests.FSharp/Data/LocalWebpage.html create mode 100644 test/ModularPipelines.UnitTests.FSharp/Data/Nest1/Nest2/Nest3/Nest4/Nest5/Blah.txt create mode 100644 test/ModularPipelines.UnitTests.FSharp/Data/Zip/Lorem.txt create mode 100644 test/ModularPipelines.UnitTests.FSharp/GlobalDummyModule.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/GlobalTestSetup.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj create mode 100644 test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/TestConstants.fs diff --git a/Directory.Packages.props b/Directory.Packages.props index 177fd86ac3..8f444a3821 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -83,6 +83,7 @@ + diff --git a/ModularPipelines.sln b/ModularPipelines.sln index 2102ff7c26..4cf893ebe2 100644 --- a/ModularPipelines.sln +++ b/ModularPipelines.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.6.33815.320 +# Visual Studio Version 18 +VisualStudioVersion = 18.5.11723.231 stable MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModularPipelines", "src\ModularPipelines\ModularPipelines.csproj", "{A25FAFCF-E226-4263-B3D6-732668604BD9}" EndProject @@ -125,6 +125,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.Syft", "sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModularPipelines.Grype", "src\ModularPipelines.Grype\ModularPipelines.Grype.csproj", "{60E4E82D-7BBF-4513-80ED-36A2273BB97D}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ModularPipelines.UnitTests.FSharp", "test\ModularPipelines.UnitTests.FSharp\ModularPipelines.UnitTests.FSharp.fsproj", "{7383D287-8BC9-4214-90A2-9B7CA4C4CBE6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -807,6 +809,18 @@ Global {60E4E82D-7BBF-4513-80ED-36A2273BB97D}.Release|x64.Build.0 = Release|Any CPU {60E4E82D-7BBF-4513-80ED-36A2273BB97D}.Release|x86.ActiveCfg = Release|Any CPU {60E4E82D-7BBF-4513-80ED-36A2273BB97D}.Release|x86.Build.0 = Release|Any CPU + {7383D287-8BC9-4214-90A2-9B7CA4C4CBE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7383D287-8BC9-4214-90A2-9B7CA4C4CBE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7383D287-8BC9-4214-90A2-9B7CA4C4CBE6}.Debug|x64.ActiveCfg = Debug|Any CPU + {7383D287-8BC9-4214-90A2-9B7CA4C4CBE6}.Debug|x64.Build.0 = Debug|Any CPU + {7383D287-8BC9-4214-90A2-9B7CA4C4CBE6}.Debug|x86.ActiveCfg = Debug|Any CPU + {7383D287-8BC9-4214-90A2-9B7CA4C4CBE6}.Debug|x86.Build.0 = Debug|Any CPU + {7383D287-8BC9-4214-90A2-9B7CA4C4CBE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7383D287-8BC9-4214-90A2-9B7CA4C4CBE6}.Release|Any CPU.Build.0 = Release|Any CPU + {7383D287-8BC9-4214-90A2-9B7CA4C4CBE6}.Release|x64.ActiveCfg = Release|Any CPU + {7383D287-8BC9-4214-90A2-9B7CA4C4CBE6}.Release|x64.Build.0 = Release|Any CPU + {7383D287-8BC9-4214-90A2-9B7CA4C4CBE6}.Release|x86.ActiveCfg = Release|Any CPU + {7383D287-8BC9-4214-90A2-9B7CA4C4CBE6}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -868,6 +882,7 @@ Global {0FB125FE-5AB3-4667-8D1B-85A6284474ED} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {2E70AA19-0309-4C6F-83D2-8E3DD2A7EC89} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {60E4E82D-7BBF-4513-80ED-36A2273BB97D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {7383D287-8BC9-4214-90A2-9B7CA4C4CBE6} = {F213898F-1E32-48F1-AB8C-83D2BD01A93B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A5905A5D-B4E1-4A7A-9279-0283D86A9F7F} diff --git a/test/ModularPipelines.UnitTests.FSharp/Data/BashTest.sh b/test/ModularPipelines.UnitTests.FSharp/Data/BashTest.sh new file mode 100644 index 0000000000..bfefcc24ec --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Data/BashTest.sh @@ -0,0 +1,2 @@ +#!/bin/bash +echo "Foo bar!" \ No newline at end of file diff --git a/test/ModularPipelines.UnitTests.FSharp/Data/Foo.txt b/test/ModularPipelines.UnitTests.FSharp/Data/Foo.txt new file mode 100644 index 0000000000..3d9fe4a2fa --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Data/Foo.txt @@ -0,0 +1 @@ +Bar! \ No newline at end of file diff --git a/test/ModularPipelines.UnitTests.FSharp/Data/LocalWebpage.html b/test/ModularPipelines.UnitTests.FSharp/Data/LocalWebpage.html new file mode 100644 index 0000000000..2aac30f4aa --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Data/LocalWebpage.html @@ -0,0 +1,10 @@ + + + + + Foo bar! + + +Foo bar! + + \ No newline at end of file diff --git a/test/ModularPipelines.UnitTests.FSharp/Data/Nest1/Nest2/Nest3/Nest4/Nest5/Blah.txt b/test/ModularPipelines.UnitTests.FSharp/Data/Nest1/Nest2/Nest3/Nest4/Nest5/Blah.txt new file mode 100644 index 0000000000..d51c6ac8b3 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Data/Nest1/Nest2/Nest3/Nest4/Nest5/Blah.txt @@ -0,0 +1 @@ +Blah! \ No newline at end of file diff --git a/test/ModularPipelines.UnitTests.FSharp/Data/Zip/Lorem.txt b/test/ModularPipelines.UnitTests.FSharp/Data/Zip/Lorem.txt new file mode 100644 index 0000000000..27b1c3b6df --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Data/Zip/Lorem.txt @@ -0,0 +1,49 @@ +Vel odio ipsum amet ea clita quis ipsum sed sed erat. Sit elitr delenit justo in ut dolor dolor option tincidunt at lorem sea aliquyam duo wisi est. Lorem vulputate magna. Dolor nihil quis ipsum tincidunt duo. Ut et eu nonumy consetetur ea tincidunt eirmod sed blandit rebum aliquyam voluptua sit vero dolor aliquam eirmod. Tempor lorem quod aliquyam invidunt esse eum eos erat dolores stet consetetur diam aliquyam vero facilisis nulla sit et. Ea voluptua nibh diam magna. Eros te dolore diam amet tempor gubergren hendrerit nostrud justo sea tempor mazim facer feugiat sadipscing rebum. Et exerci invidunt. Accusam duo amet sit ea diam. Dolores vulputate ut et eum stet dolor diam dolore. At duo ipsum esse lorem velit est et commodo. Odio sit tempor kasd takimata diam at ut zzril dolore amet et tempor lorem elitr ipsum. Hendrerit clita diam nibh molestie at est. Velit aliquip dolor dolores zzril sed duis est eirmod duo ipsum tempor suscipit sadipscing facilisis eirmod nostrud nostrud. + +Est feugiat amet aliquyam takimata facilisi amet tation et facer labore labore et. Dolor invidunt sed. Sit sit assum et ipsum invidunt sit sanctus takimata dolor lorem in dolores erat at. + +Vero dolor sit gubergren sea amet invidunt labore dolores et sit tempor dolor takimata vulputate. Est nostrud lorem aliquyam vulputate blandit stet nibh sed et tempor at commodo ut sanctus. Sit ut sanctus wisi diam et elitr amet nisl magna delenit ipsum clita vero accumsan nonumy delenit. + +Molestie est takimata sed invidunt eos sadipscing dolore feugait. Facilisis dignissim consetetur diam eirmod feugait ea erat accusam. Voluptua volutpat dolor sit sea justo no sed eos nulla dolor eros dolor. Tempor elitr sanctus diam. Kasd ut rebum. Placerat velit clita et vero dolore rebum sea gubergren hendrerit rebum lobortis accusam. Amet dignissim est dolore ipsum in dolores aliquyam tempor velit. Eos sit lobortis et elitr gubergren rebum kasd et ipsum nam dolor lorem diam at exerci lorem diam lorem. Takimata et gubergren consetetur assum nobis commodo amet est sit kasd consequat in. Lorem dignissim erat et duo enim. Quod sanctus ipsum. Velit sanctus adipiscing diam ipsum duo at labore lorem dolore sed ut consetetur et no. Consectetuer tincidunt eleifend et et nonumy rebum sadipscing in. Praesent tempor dolores eirmod stet diam takimata tempor rebum erat stet eos dolor volutpat esse lorem id. Clita vel ut kasd elitr tempor velit et eos eu diam sit consetetur takimata tation diam. Velit accusam nibh augue delenit sed vero sed magna et gubergren eos labore ipsum. + +Invidunt magna qui consetetur est sed magna dolor. Erat consetetur ut lorem dolor. Ea stet dolore duis erat ea ad dolore facilisi clita assum duis tempor et veniam diam rebum diam accusam. At ipsum amet sed eu diam hendrerit justo diam praesent odio sadipscing sed diam sed kasd sit in amet. Facer dolore clita et invidunt luptatum. Dolor rebum kasd magna. Diam amet magna. Dolores erat sit nonumy eos dolor et nonumy takimata amet illum gubergren dolor diam possim nisl stet no. Sadipscing in stet tempor stet stet et ipsum iriure erat illum amet. + +Et eum rebum at takimata laoreet sit doming eos. Sed ipsum velit vel no. Sadipscing consectetuer ea tincidunt eirmod lorem est. Tempor elitr diam est gubergren diam ullamcorper iriure lorem erat ea amet voluptua. Magna sit possim ut qui est velit et iriure nisl amet justo sanctus lorem ea. Eum clita esse justo tempor ea adipiscing diam ut qui et aliquyam ipsum nisl dolor ea justo. + +Dolores eirmod vulputate illum consectetuer sadipscing et wisi suscipit kasd. Voluptua eros sit sea lorem dolor elitr facilisi sed et luptatum congue magna. Accusam voluptua dolores eirmod ut aliquyam wisi amet clita feugiat lorem ipsum sit dolor at dolore erat aliquyam aliquyam. Euismod dolor ut sed diam amet eos. Labore hendrerit dolor facilisi sadipscing illum. Ut voluptua nibh lorem no et at. Consectetuer lorem ex laoreet justo diam euismod zzril dolore enim ex accusam ea. Voluptua ut eros sea et amet et dolore rebum at iriure vero nonumy clita sed rebum. Lorem justo no magna et invidunt nonumy erat et. Eos autem nonumy accumsan nonumy ipsum duo et sanctus eos voluptua sit id nostrud ipsum no accusam. Ea nulla amet accumsan takimata et duis dolore sed consectetuer accumsan lorem ullamcorper exerci sanctus at lorem magna euismod. Sit no et et invidunt sed rebum ipsum consetetur blandit duo clita rebum at ipsum nisl vero et iriure. Vero sea takimata dolore ipsum. Vel autem amet justo quis est et dolores duis est nisl et hendrerit lorem amet consequat sanctus et dolor. Labore erat est ut ut zzril sea enim aliquyam quod. Eirmod kasd sed vulputate luptatum sea at sea rebum sed ipsum duis ipsum sit tincidunt erat mazim est. Wisi ut ea voluptua vero diam tincidunt gubergren et kasd sit. + +Lorem ipsum qui takimata kasd. Accusam nonumy invidunt zzril lorem invidunt invidunt ut diam labore kasd ut ea ad. Erat duo nam ut est diam eirmod dolor sed erat. Diam amet sadipscing clita lorem praesent vulputate amet labore takimata erat lorem amet et stet. Dolor voluptua eos voluptua magna nisl ipsum voluptua ut rebum lorem. Diam et sadipscing autem ipsum tempor gubergren te veniam quis qui sanctus et sit. Duis amet sed kasd minim praesent et invidunt lorem justo est tempor. Sed et diam invidunt diam magna takimata sit dolor commodo amet nulla dolore. Elitr eos sea consetetur feugiat zzril diam. Voluptua et dolor erat velit takimata. Eirmod ut ipsum adipiscing et qui sed lorem sit ut dolore sed at autem gubergren vel dignissim. Takimata sed stet dolor vero voluptua possim sed ut sea vulputate sit sed nostrud duo dolor no. Dolor eos clita. Sed aliquip in feugiat elitr aliquyam praesent elitr sadipscing et amet lorem eu amet et tation praesent et. Hendrerit labore stet clita dolores amet voluptua sea nonumy ipsum at consetetur duis amet magna sadipscing sadipscing. Wisi ut ut facilisis sadipscing rebum dolor et illum diam dolor. Et sadipscing magna at ut eros sit takimata vero in lorem ipsum. + +Dignissim dolor lorem magna praesent eos sed est tempor invidunt amet dolor. Eos iriure volutpat ipsum lorem diam nisl est lorem stet labore dolores est feugiat. Adipiscing velit suscipit ipsum dolor vulputate minim elitr nulla tempor gubergren delenit rebum. Et nonummy et laoreet ipsum nonumy magna at aliquam lorem hendrerit vero dolores laoreet aliquyam cum ea. At ipsum hendrerit illum zzril elitr erat consectetuer nibh eirmod. Amet vulputate at nonumy et diam. Sit minim consequat nibh nonumy sanctus sit sanctus amet at accumsan diam dolore kasd accusam. Nonumy ullamcorper suscipit feugait aliquyam sed sed blandit diam. + +Eirmod ullamcorper et ut accumsan autem nihil ipsum. Voluptua ipsum eleifend lorem aliquyam. Wisi aliquip clita dolor tation eros sed sadipscing volutpat. + +Velit stet no ut justo ipsum. Sit dolor ea sed tincidunt et ad rebum hendrerit et accusam sed no dolores erat est vel. Vulputate nonumy sit dolor no ut. Tempor dolor gubergren sea consetetur ea sea amet lorem invidunt labore erat eirmod eos aliquyam. Eum lorem no sanctus dolore diam lorem vero ea elit elitr sit no erat lorem tempor. Rebum eos stet lobortis. Volutpat ut rebum et aliquam magna sit. Takimata lorem amet autem dolor dolore amet kasd labore ea ipsum consequat amet doming consetetur magna hendrerit. Et nulla no vero adipiscing kasd ipsum at sit. Sadipscing sanctus praesent eos sed et gubergren nonumy sit diam. Eos velit esse rebum takimata et illum kasd et elitr consequat consectetuer ipsum et lorem stet duo diam. + +Erat magna diam dolor. Vel voluptua mazim laoreet ut est aliquyam dolore. Tincidunt sanctus euismod dolor in amet accusam consequat erat sit ut. Aliquip takimata clita no diam dolor accusam qui nonumy. Gubergren invidunt illum vel diam voluptua eirmod molestie clita est dolores delenit takimata eirmod takimata possim. Augue eirmod ea dolore at kasd eos sed et nonummy. No lorem nostrud vel eos velit justo velit consetetur gubergren. Iriure id in magna est accusam diam wisi at dignissim. Euismod sea feugiat stet ipsum labore et duis minim accusam enim. Augue ut eum aliquam elitr accusam justo blandit amet sanctus eirmod sadipscing et erat. Velit luptatum velit et et. Erat dolor sit. Sadipscing vel ut erat eleifend vel lorem lorem invidunt sed dolore ullamcorper ea minim clita sanctus duo gubergren ipsum. Eirmod sadipscing at justo invidunt kasd elitr et in amet sed ipsum stet nisl lorem at ut eirmod amet. Duo sed cum dolor aliquyam ea ipsum sea. Rebum at duis ipsum erat eum dolores accusam iriure sadipscing. Nulla ipsum velit invidunt invidunt ea aliquyam amet. + +Et aliquyam lorem iusto volutpat no nulla eos. Accusam stet ut stet nibh sed ipsum lorem dolore kasd et ipsum diam amet diam dolores ea sea. Luptatum rebum duo dolor feugait labore sadipscing amet. Amet iriure tempor nibh ipsum consectetuer et sed eos in gubergren nibh accusam kasd. Ut consequat magna dolore ut at aliquip ut dolor sed sed erat ipsum dolore consetetur sed sit dolor. Sea suscipit vero euismod lorem et vero et amet quod ipsum invidunt consetetur lorem est dolores voluptua dolore clita. Nonumy vulputate diam ut. In tation justo aliquyam vel veniam no amet accusam. Vero clita sea et tincidunt consetetur at te stet aliquam est dolor no dolore facilisis ut. Diam invidunt clita justo wisi magna diam dolor ea sanctus. Ea dolore aliquyam et vero ipsum stet est duo et justo adipiscing dolor ea sed dolore nibh stet ipsum. + +Ad sit justo dolor no labore stet ullamcorper duo. Diam eirmod takimata. Magna dolores ut diam eos invidunt magna dolores option accusam magna molestie ipsum molestie sadipscing aliquyam et. Ut duo dolor invidunt stet at. Dolor sit duo amet dolor et et in et dolor rebum sed diam vero vel amet imperdiet et. Diam quod magna vel rebum et invidunt gubergren ipsum takimata elitr amet dolor diam at in labore diam. + +Veniam diam sadipscing et est aliquyam duo dolor at eirmod ut. Ipsum diam erat facer eirmod et sanctus kasd kasd kasd diam labore sed. Dolores labore ea blandit. Dolore dolor stet sit iusto option. Dolor lorem est elitr lorem et et amet elitr takimata dolor cum ipsum sed. At sadipscing dolore sit gubergren sanctus ipsum stet labore quis at consectetuer lorem feugiat diam et feugait diam. + +Takimata nam duo et. Amet diam gubergren gubergren. Elit molestie sed accumsan diam hendrerit justo. Velit elitr sit tempor liber imperdiet tincidunt suscipit ut eirmod invidunt ut et augue. + +Consetetur justo tincidunt ut aliquyam kasd sed sadipscing dolor eu labore. Elitr sadipscing sed et ipsum dolores justo elit ut et sit at. Et ea accumsan quod at ut et eos lorem elit duo rebum dolore accusam. Rebum no labore voluptua clita. Rebum at no et kasd est consetetur nostrud takimata vero duo labore est justo diam eirmod erat diam. Dolores diam sadipscing augue gubergren suscipit amet sit euismod qui sed invidunt vero sed dolor takimata justo ea. Sadipscing aliquyam iusto feugiat quis sanctus eleifend. Dolores dolor ipsum te sed accusam magna. Gubergren eirmod stet lorem nonumy dolores tincidunt sit sadipscing dolor stet sadipscing accusam sed stet. Iusto vulputate labore elit feugiat no luptatum et nonumy vulputate dolore gubergren sed. Sit wisi sadipscing commodo. Duo diam luptatum vel diam sit et ut rebum ut nam no. Eos velit et aliquam est et dolores sit est et lorem assum. Volutpat consetetur duo dolor sanctus sit magna eos consetetur dolore accusam et sea sit no iriure accusam. + +Amet facilisis accusam esse ipsum ea gubergren. Amet dolor consectetuer diam clita consetetur tincidunt et feugait labore et. Sanctus consequat ipsum sed placerat ut sadipscing rebum justo voluptua nulla ut autem dolore. Ut duis ipsum ipsum minim nulla aliquam nonumy eirmod et voluptua no dolore commodo voluptua zzril. Lorem sed dolores augue eos sed ipsum lorem vero sit diam iriure no commodo hendrerit. Ipsum dolor iriure est. Ipsum eu dolore dolor ea et. Et consetetur elit quod tation duo magna. Nulla dolor tempor et hendrerit. Elitr illum imperdiet ea erat. Ut erat tincidunt consequat dolor dolore. Vel dolor nobis ad no sed sit dolor stet erat nobis. Diam praesent sanctus amet sit duis. Aliquyam sea consetetur dolore et nobis ea amet nonummy sit sit tempor sed ea duis no velit rebum diam. Sadipscing et dolore erat. Consetetur amet nonumy labore sed erat eu delenit et lorem consectetuer dolor rebum aliquyam. Sed iriure aliquip sit sanctus kasd kasd et erat diam dolores invidunt duo accusam autem est sadipscing ea. + +Duo erat dolores soluta dolores lorem tation amet illum rebum et at dolores clita tempor vel. Eu amet consetetur ea ad dolore dolor kasd. Wisi elitr ad amet. Tempor tempor justo ipsum nibh eu dolor ea et odio ea sit ea wisi voluptua justo. Labore et dolores te duis qui id et wisi lorem in vero eos veniam ipsum. At ad aliquam dolore rebum sadipscing diam cum aliquyam diam vulputate tempor lorem. + +Cum et sed sea ea diam minim magna ipsum hendrerit duis clita voluptua. Sed amet takimata duo diam diam dolor sit tempor at sed nihil. Nulla erat sed takimata et. Ipsum eirmod luptatum. Accusam vel imperdiet illum sed tempor diam. Magna sed at dolor takimata elit et laoreet in lorem et lorem labore et tempor stet autem erat. Clita in vero takimata elitr autem est invidunt qui amet. Diam eleifend augue et gubergren et velit diam consetetur. + +Dolores ea et accusam nihil clita sanctus aliquyam eu sed ut congue ipsum amet. Est luptatum et ipsum autem duo et ea clita duo nulla illum takimata et vero accusam tempor sed. Sadipscing tincidunt labore. Dolor sadipscing magna exerci praesent consetetur dolor ipsum vero sed sit minim eos volutpat diam consequat. Amet sit kasd et nonumy ea lorem stet no diam ipsum est et ipsum lorem et enim sed no. At hendrerit et no eos elit lorem tempor invidunt. Ut dolor exerci diam ipsum gubergren placerat amet sea minim magna amet et duo iusto eu sit. Labore gubergren nonummy tempor option et erat sanctus dolore justo facilisi invidunt sit elit sed takimata hendrerit duo vero. Lorem nonumy ipsum ea justo dolor vulputate. Stet dolor liber eleifend amet erat ea. Hendrerit elitr zzril facilisis autem et erat eos in ea dolores qui amet duo. Lorem nonumy eirmod accusam labore dolore stet takimata blandit augue vel vero sit feugiat takimata. Consectetuer ea duo consequat lorem odio dolor. Eum no at. Dolores ipsum dolores aliquyam amet at sit clita amet ut ea erat. + +Clita et dolores. Et in molestie voluptua feugiat est magna ut. Et sed diam eum invidunt suscipit takimata sed diam duis aliquip vel lorem dolore. Feugiat clita option diam et stet accumsan dolore sed stet. Labore wisi diam. Augue lorem in consequat dolor stet esse rebum. Elitr sanctus amet quod diam et labore vel nonumy sea. Diam et hendrerit rebum sit no no nulla sed dolor stet ut amet ut ipsum. Ipsum accusam erat et no vel nonumy minim et justo et nostrud rebum augue. Nonumy quis dolor rebum at sed nulla tempor consetetur sed aliquyam ipsum dolor et. Iriure tempor aliquyam ex sed hendrerit dolores ea est dolore dolore kasd dolor sed sadipscing enim consetetur dolore. Dolore vel tincidunt. Vero gubergren at labore justo rebum erat sed vero id duo vero sanctus takimata et. Nam iusto dolor diam magna nostrud erat eu tincidunt amet eu invidunt accusam dolor accumsan dolores. Nulla ea possim tempor takimata eos amet et et et dolor aliquyam accusam duis takimata. + +Kasd ullamcorper tempor duo vel labore vel sadipscing sed. Eleifend vero takimata commodo ut commodo duo minim elitr sadipscing. Autem no sea eu diam molestie eum amet nam. At vulputate et ut amet ea est amet. Sed kasd dolores iriure invidunt ipsum eos elitr zzril dolor ipsum et lorem consequat dolores stet ut sed consectetuer. Eum dolores euismod soluta sit voluptua ipsum ut nostrud consequat vulputate clita volutpat kasd sit. Dolor veniam eos illum lorem aliquyam dolore invidunt lobortis blandit diam eirmod sanctus eos ipsum. Eirmod consequat ea. Invidunt et sadipscing gubergren amet nonumy facilisis sed gubergren sed erat tempor consectetuer eum ea tempor nonumy dolor nonumy. Ut invidunt lorem labore lorem zzril clita velit tempor ex. Lorem dolore voluptua duo eu. Stet hendrerit duis ex ipsum stet minim. + +Accusam kasd sea rebum luptatum ipsum. Dolore nonumy qui amet gubergren kasd stet diam vero sed feugiat. Magna at vero sit sit labore et nulla in ut dolore et tempor duis voluptua rebum dolor. In et ipsum dolor consetetur aliquam lorem vel voluptua amet. Accusam diam clita ut erat vero ipsum eum sed vulputate eu sed facer diam sed in augue delenit ipsum. Sit quod feugait. Doming clita erat magna tempor consequat duo takimata eirmod tempor blandit ut. Tempor voluptua aliquam diam commodo ex sea. Elitr lorem at sit dolore tempor duo sed praesent at dolor dolores delenit sea dolores amet duo. Invidunt accusam odio ipsum invidunt nulla takimata vel et nonumy consetetur. Volutpat nonumy dolores sit ea. Et ut est kasd lorem erat et euismod voluptua vero sed vel elitr at gubergren. + +Tempor duis diam stet dolor diam illum amet commodo ut sea diam. Sit dolor ipsum aliquam hendrerit dolore liber no accusam in aliquyam et. Dolore at stet dolor ea duis voluptua in aliquyam magna sed dolores laoreet nonummy iriure ea. Autem dolor dolore lorem eos diam takimata tempor diam diam liber. Et et no et eros sea dolor eos dolor sit. Sit et accusam suscipit diam liber stet magna stet sadipscing et sit nobis diam takimata sadipscing dolores est. Tation et accusam magna rebum blandit justo magna ea aliquam. Commodo duo sea eirmod dolore dolore dolor lorem magna. Gubergren at iriure. Veniam at voluptua erat eirmod duis ut sadipscing. Diam tempor erat invidunt sed est no. Ipsum ipsum in sed duo et ipsum magna at in autem at dolore duo. Dolor labore ut sadipscing veniam sit imperdiet nonumy liber nostrud gubergren. Dolores eos sit elit dolore et suscipit. Nonummy ipsum diam duis accusam erat sed clita tempor est duo. Eros lorem ea invidunt eos stet wisi sea clita sea labore sit in vulputate erat eleifend at. Amet lorem ipsum et. \ No newline at end of file diff --git a/test/ModularPipelines.UnitTests.FSharp/GlobalDummyModule.fs b/test/ModularPipelines.UnitTests.FSharp/GlobalDummyModule.fs new file mode 100644 index 0000000000..663fe3a552 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/GlobalDummyModule.fs @@ -0,0 +1,9 @@ +namespace ModularPipelines.UnitTests.FSharp + +open ModularPipelines.TestHelpers +open System.Collections.Generic + +type GlobalDummyModule() = + inherit SimpleTestModule | null>() + + override _.Result : IDictionary | null = null \ No newline at end of file diff --git a/test/ModularPipelines.UnitTests.FSharp/GlobalTestSetup.fs b/test/ModularPipelines.UnitTests.FSharp/GlobalTestSetup.fs new file mode 100644 index 0000000000..1a8b66a6a2 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/GlobalTestSetup.fs @@ -0,0 +1,11 @@ +namespace ModularPipelines.UnitTests.FSharp + +open System +open System.Diagnostics.CodeAnalysis +open TUnit.Core + +type GlobalHooks() = + [] + static member SetUp() = + Environment.CurrentDirectory <- TestContext.OutputDirectory; + diff --git a/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj b/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj new file mode 100644 index 0000000000..8cde2067ce --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj @@ -0,0 +1,65 @@ + + + + latest + enable + enable + Exe + net10.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs b/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs new file mode 100644 index 0000000000..46167a2098 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs @@ -0,0 +1,17 @@ +namespace ModularPipelines.UnitTests.FSharp + +/// +/// Tests for large-scale pipeline scenarios to validate scalability, +/// performance, and correct behavior with many modules. +/// +/// +/// These tests verify that the pipeline engine handles large numbers of modules +/// correctly, including proper parallelization, dependency ordering, and resource management. +/// +/// Note: ModularPipelines requires each module to be a unique type (by design, +/// dependencies are resolved by type). These tests use generic types with different +/// type parameters to create many unique module types at compile time. +/// +[] +type ScaleTests() = + \ No newline at end of file diff --git a/test/ModularPipelines.UnitTests.FSharp/TestConstants.fs b/test/ModularPipelines.UnitTests.FSharp/TestConstants.fs new file mode 100644 index 0000000000..0c0465748f --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/TestConstants.fs @@ -0,0 +1,11 @@ +namespace ModularPipelines.UnitTests.FSharp + +module TestConstants = + [] + let TestString = "Foo bar!" + + [] + let ErrorPrefix = "Error: " + + [] + let RequirementErrorMessage = ErrorPrefix + TestString \ No newline at end of file From b6557949f4e4d42972be79c8c9aa637b47a11f87 Mon Sep 17 00:00:00 2001 From: James Alickolli Date: Wed, 13 May 2026 11:07:09 +1000 Subject: [PATCH 02/14] Added ScaleTest to FSharp --- Directory.Packages.props | 1 + .../ModularPipelines.UnitTests.FSharp.fsproj | 10 +- .../ScaleTests.fs | 291 +++++++++++++++++- 3 files changed, 283 insertions(+), 19 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8f444a3821..25ff995703 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -37,6 +37,7 @@ + diff --git a/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj b/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj index 8cde2067ce..1e0a998c81 100644 --- a/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj +++ b/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj @@ -4,6 +4,7 @@ latest enable enable + false Exe net10.0 @@ -12,6 +13,7 @@ + @@ -26,14 +28,14 @@ - - - - + + + + diff --git a/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs b/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs index 46167a2098..74fa757f12 100644 --- a/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs +++ b/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs @@ -1,17 +1,278 @@ namespace ModularPipelines.UnitTests.FSharp -/// -/// Tests for large-scale pipeline scenarios to validate scalability, -/// performance, and correct behavior with many modules. -/// -/// -/// These tests verify that the pipeline engine handles large numbers of modules -/// correctly, including proper parallelization, dependency ordering, and resource management. -/// -/// Note: ModularPipelines requires each module to be a unique type (by design, -/// dependencies are resolved by type). These tests use generic types with different -/// type parameters to create many unique module types at compile time. -/// -[] -type ScaleTests() = - \ No newline at end of file +open ModularPipelines.TestHelpers +open System.Collections.Concurrent +open System.Reflection +open System.Threading +open ModularPipelines.Modules +open ModularPipelines.Context +open TUnit.Core +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open ModularPipelines.Extensions +open Microsoft.Extensions.DependencyInjection +open ModularPipelines.Enums + +module ScaleTestsModule = + type ExecutionRecord = { + ModuleName: string + CompletionOrder : int + StartTime: System.DateTimeOffset + EndTime: System.DateTimeOffset + } + /// + /// Thread-safe tracker for recording module execution order and timing. Each test creates a fresh instance for test + /// isolation. + /// + type ExecutionTracker() = + let _executionRecords = ConcurrentDictionary() + let mutable _completionCounter = 0 + + /// + /// Records the start of module execution. + /// + /// The name/type of the module. + member _.RecordStart(moduleName: string) = + let record = { ModuleName = moduleName; CompletionOrder = 0; StartTime = System.DateTimeOffset.UtcNow; EndTime = System.DateTimeOffset.MinValue } + _executionRecords[moduleName] <- record + /// + /// Marks a module as completed and records completion order. + /// + /// The module name. + member _.MarkCompleted(moduleName: string) = + let completionOrder = Interlocked.Increment(ref _completionCounter); + match _executionRecords.TryGetValue(moduleName) with + | true, record -> + let updatedRecord = { record with CompletionOrder = completionOrder; EndTime = System.DateTimeOffset.UtcNow } + _executionRecords[moduleName] <- updatedRecord + | false, _ -> () + + /// + /// Checks if a module has completed. + /// + /// The module name. + /// True if the module has completed. + member _.IsCompleted(moduleName: string) = + match _executionRecords.TryGetValue(moduleName) with + | true, record -> record.EndTime <> System.DateTimeOffset.MinValue + | false, _ -> false + + /// + /// Gets all execution records ordered by completion order. + /// + member _.GetRecords() = + _executionRecords.Values + |> Seq.sortBy (fun r -> r.CompletionOrder) + |> Seq.toList + + /// + /// Gets a specific execution record by module name. + /// + member _.GetRecord(moduleName: string) = + match _executionRecords.TryGetValue(moduleName) with + | true, record -> Some record + | false, _ -> None + + /// + /// Gets the count of completed modules. + /// + member _.CompletedCount() = + _executionRecords.Values |> Seq.sumBy (fun r -> if r.CompletionOrder > 0 then 1 else 0) + + /// + /// Gets the total count of recorded modules (started or completed). + /// + member _.TotalCount() = _executionRecords.Count + + /// + /// Gets whether the tracker is in a clean state (no records). + /// + member _.IsClean() = + _executionRecords.IsEmpty && _completionCounter = 0 + + [] type M1 = struct end + [] type M2 = struct end + [] type M3 = struct end + [] type M4 = struct end + [] type M5 = struct end + [] type M6 = struct end + [] type M7 = struct end + [] type M8 = struct end + [] type M9 = struct end + [] type M10 = struct end + [] type M11 = struct end + [] type M12 = struct end + [] type M13 = struct end + [] type M14 = struct end + [] type M15 = struct end + [] type M16 = struct end + [] type M17 = struct end + [] type M18 = struct end + [] type M19 = struct end + [] type M20 = struct end + [] type M21 = struct end + [] type M22 = struct end + [] type M23 = struct end + [] type M24 = struct end + [] type M25 = struct end + [] type M26 = struct end + [] type M27 = struct end + [] type M28 = struct end + [] type M29 = struct end + [] type M30 = struct end + [] type M31 = struct end + [] type M32 = struct end + [] type M33 = struct end + [] type M34 = struct end + [] type M35 = struct end + [] type M36 = struct end + [] type M37 = struct end + [] type M38 = struct end + [] type M39 = struct end + [] type M40 = struct end + [] type M41 = struct end + [] type M42 = struct end + [] type M43 = struct end + [] type M44 = struct end + [] type M45 = struct end + [] type M46 = struct end + [] type M47 = struct end + [] type M48 = struct end + [] type M49 = struct end + [] type M50 = struct end + [] type M51 = struct end + [] type M52 = struct end + [] type M53 = struct end + [] type M54 = struct end + [] type M55 = struct end + [] type M56 = struct end + [] type M57 = struct end + [] type M58 = struct end + [] type M59 = struct end + [] type M60 = struct end + [] type M61 = struct end + [] type M62 = struct end + [] type M63 = struct end + [] type M64 = struct end + [] type M65 = struct end + [] type M66 = struct end + [] type M67 = struct end + [] type M68 = struct end + [] type M69 = struct end + [] type M70 = struct end + [] type M71 = struct end + [] type M72 = struct end + [] type M73 = struct end + [] type M74 = struct end + [] type M75 = struct end + [] type M76 = struct end + [] type M77 = struct end + [] type M78 = struct end + [] type M79 = struct end + [] type M80 = struct end + [] type M81 = struct end + [] type M82 = struct end + [] type M83 = struct end + [] type M84 = struct end + [] type M85 = struct end + [] type M86 = struct end + [] type M87 = struct end + [] type M88 = struct end + [] type M89 = struct end + [] type M90 = struct end + [] type M91 = struct end + [] type M92 = struct end + [] type M93 = struct end + [] type M94 = struct end + [] type M95 = struct end + [] type M96 = struct end + [] type M97 = struct end + [] type M98 = struct end + [] type M99 = struct end + [] type M100 = struct end + + /// + /// A generic module that can be instantiated with different type markers to create unique types. + /// + /// A marker type that makes this module unique. + type ScaleModule<'T>(tracker: ExecutionTracker) = + inherit Module() + override _.ExecuteAsync(context: IModuleContext, cancellationToken: System.Threading.CancellationToken) = + task { + let typeName = typeof<'T>.Name + tracker.RecordStart(typeName); + tracker.MarkCompleted(typeName); + return typeName; + } + + /// + /// Tests for large-scale pipeline scenarios to validate scalability, performance, and correct behavior with many + /// modules. + /// + /// + /// These tests verify that the pipeline engine handles large numbers of modules correctly, including proper + /// parallelization, dependency ordering, and resource management. Note: ModularPipelines requires each module to be + /// a unique type (by design, dependencies are resolved by type). These tests use generic types with different type + /// parameters to create many unique module types at compile time. + /// + [] + type ScaleTests() = + inherit TestBase() + + /// + /// Verifies that a pipeline with 100 independent modules completes successfully and all modules execute. + /// + /// + /// This test validates: - The pipeline can handle 100 modules without issues - All modules complete + /// successfully - The pipeline status is successful + /// + [] + member _.Pipeline_With100IndependentModules_CompletesSuccessfully() = + async { + let tracker = new ExecutionTracker(); + do! check(Assert.That(tracker.IsClean).IsTrue()) + let expectedModuleCount = 100 + let builder = + TestPipelineHostBuilder.Create() + .ConfigureServices(fun _ services -> services.AddSingleton(tracker) |> ignore) + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>().AddModule>().AddModule>() + .AddModule>() + + let! pipelineSummary = builder.ExecutePipelineAsync() |> Async.AwaitTask + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(tracker.CompletedCount()), expectedModuleCount)) + } + From 251463c7eaf9b4e51662041887f596256cbc5af6 Mon Sep 17 00:00:00 2001 From: James Alickolli Date: Wed, 13 May 2026 12:06:23 +1000 Subject: [PATCH 03/14] Update ScaleTests.fs --- .../ScaleTests.fs | 664 ++++++++++++++++++ 1 file changed, 664 insertions(+) diff --git a/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs b/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs index 74fa757f12..3ace2fa4e0 100644 --- a/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs +++ b/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs @@ -205,6 +205,606 @@ module ScaleTestsModule = tracker.MarkCompleted(typeName); return typeName; } + [] type C1 = struct end + [] type C2 = struct end + [] type C3 = struct end + [] type C4 = struct end + [] type C5 = struct end + [] type C6 = struct end + [] type C7 = struct end + [] type C8 = struct end + [] type C9 = struct end + [] type C10 = struct end + [] type C11 = struct end + [] type C12 = struct end + [] type C13 = struct end + [] type C14 = struct end + [] type C15 = struct end + [] type C16 = struct end + [] type C17 = struct end + [] type C18 = struct end + [] type C19 = struct end + [] type C20 = struct end + [] type C21 = struct end + [] type C22 = struct end + [] type C23 = struct end + [] type C24 = struct end + [] type C25 = struct end + [] type C26 = struct end + [] type C27 = struct end + [] type C28 = struct end + [] type C29 = struct end + [] type C30 = struct end + [] type C31 = struct end + [] type C32 = struct end + [] type C33 = struct end + [] type C34 = struct end + [] type C35 = struct end + [] type C36 = struct end + [] type C37 = struct end + [] type C38 = struct end + [] type C39 = struct end + [] type C40 = struct end + [] type C41 = struct end + [] type C42 = struct end + [] type C43 = struct end + [] type C44 = struct end + [] type C45 = struct end + [] type C46 = struct end + [] type C47 = struct end + [] type C48 = struct end + [] type C49 = struct end + [] type C50 = struct end + + // Chain modules - each depends on the previous one + type ChainModule1(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain1") + tracker.MarkCompleted("Chain1") + return 1 + } + + [)>] + type ChainModule2(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain2") + tracker.MarkCompleted("Chain2") + return 2 + } + + [)>] + type ChainModule3(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain3") + tracker.MarkCompleted("Chain3") + return 3 + } + + [)>] + type ChainModule4(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain4") + tracker.MarkCompleted("Chain4") + return 4 + } + + [)>] + type ChainModule5(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain5") + tracker.MarkCompleted("Chain5") + return 5 + } + + [)>] + type ChainModule6(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain6") + tracker.MarkCompleted("Chain6") + return 6 + } + + [)>] + type ChainModule7(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain7") + tracker.MarkCompleted("Chain7") + return 7 + } + + [)>] + type ChainModule8(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain8") + tracker.MarkCompleted("Chain8") + return 8 + } + + [)>] + type ChainModule9(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain9") + tracker.MarkCompleted("Chain9") + return 9 + } + + [)>] + type ChainModule10(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain10") + tracker.MarkCompleted("Chain10") + return 10 + } + + [)>] + type ChainModule11(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain11") + tracker.MarkCompleted("Chain11") + return 11 + } + + [)>] + type ChainModule12(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain12") + tracker.MarkCompleted("Chain12") + return 12 + } + + [)>] + type ChainModule13(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain13") + tracker.MarkCompleted("Chain13") + return 13 + } + + [)>] + type ChainModule14(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain14") + tracker.MarkCompleted("Chain14") + return 14 + } + + [)>] + type ChainModule15(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain15") + tracker.MarkCompleted("Chain15") + return 15 + } + + [)>] + type ChainModule16(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain16") + tracker.MarkCompleted("Chain16") + return 16 + } + + [)>] + type ChainModule17(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain17") + tracker.MarkCompleted("Chain17") + return 17 + } + + [)>] + type ChainModule18(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain18") + tracker.MarkCompleted("Chain18") + return 18 + } + + [)>] + type ChainModule19(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain19") + tracker.MarkCompleted("Chain19") + return 19 + } + + [)>] + type ChainModule20(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain20") + tracker.MarkCompleted("Chain20") + return 20 + } + + [)>] + type ChainModule21(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain21") + tracker.MarkCompleted("Chain21") + return 21 + } + + [)>] + type ChainModule22(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain22") + tracker.MarkCompleted("Chain22") + return 22 + } + + [)>] + type ChainModule23(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain23") + tracker.MarkCompleted("Chain23") + return 23 + } + + [)>] + type ChainModule24(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain24") + tracker.MarkCompleted("Chain24") + return 24 + } + + [)>] + type ChainModule25(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain25") + tracker.MarkCompleted("Chain25") + return 25 + } + + [)>] + type ChainModule26(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain26") + tracker.MarkCompleted("Chain26") + return 26 + } + + [)>] + type ChainModule27(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain27") + tracker.MarkCompleted("Chain27") + return 27 + } + + [)>] + type ChainModule28(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain28") + tracker.MarkCompleted("Chain28") + return 28 + } + + [)>] + type ChainModule29(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain29") + tracker.MarkCompleted("Chain29") + return 29 + } + + [)>] + type ChainModule30(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain30") + tracker.MarkCompleted("Chain30") + return 30 + } + + [)>] + type ChainModule31(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain31") + tracker.MarkCompleted("Chain31") + return 31 + } + + [)>] + type ChainModule32(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain32") + tracker.MarkCompleted("Chain32") + return 32 + } + + [)>] + type ChainModule33(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain33") + tracker.MarkCompleted("Chain33") + return 33 + } + + [)>] + type ChainModule34(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain34") + tracker.MarkCompleted("Chain34") + return 34 + } + + [)>] + type ChainModule35(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain35") + tracker.MarkCompleted("Chain35") + return 35 + } + + [)>] + type ChainModule36(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain36") + tracker.MarkCompleted("Chain36") + return 36 + } + + [)>] + type ChainModule37(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain37") + tracker.MarkCompleted("Chain37") + return 37 + } + + [)>] + type ChainModule38(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain38") + tracker.MarkCompleted("Chain38") + return 38 + } + + [)>] + type ChainModule39(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain39") + tracker.MarkCompleted("Chain39") + return 39 + } + + [)>] + type ChainModule40(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain40") + tracker.MarkCompleted("Chain40") + return 40 + } + + [)>] + type ChainModule41(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain41") + tracker.MarkCompleted("Chain41") + return 41 + } + + [)>] + type ChainModule42(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain42") + tracker.MarkCompleted("Chain42") + return 42 + } + + [)>] + type ChainModule43(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain43") + tracker.MarkCompleted("Chain43") + return 43 + } + + [)>] + type ChainModule44(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain44") + tracker.MarkCompleted("Chain44") + return 44 + } + + [)>] + type ChainModule45(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain45") + tracker.MarkCompleted("Chain45") + return 45 + } + + [)>] + type ChainModule46(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain46") + tracker.MarkCompleted("Chain46") + return 46 + } + + [)>] + type ChainModule47(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain47") + tracker.MarkCompleted("Chain47") + return 47 + } + + [)>] + type ChainModule48(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain48") + tracker.MarkCompleted("Chain48") + return 48 + } + + [)>] + type ChainModule49(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain49") + tracker.MarkCompleted("Chain49") + return 49 + } + + [)>] + type ChainModule50(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("Chain50") + tracker.MarkCompleted("Chain50") + return 50 + } /// /// Tests for large-scale pipeline scenarios to validate scalability, performance, and correct behavior with many @@ -275,4 +875,68 @@ module ScaleTestsModule = do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(tracker.CompletedCount()), expectedModuleCount)) } + + /// + /// Verifies that a pipeline with a 50-module deep dependency chain + /// executes modules in the correct order. + /// + /// + /// This test validates: + /// - Deep dependency chains are resolved correctly + /// - Modules execute in dependency order + /// - No deadlocks occur with long chains + /// + [] + member _.Pipeline_With50ModuleDeepChain_CompletesInOrder() = + async{ + let tracker = new ExecutionTracker(); + do! check(Assert.That(tracker.IsClean).IsTrue()) + let chainDepth = 50; + let builder = + TestPipelineHostBuilder.Create() + .ConfigureServices(fun _ services -> services.AddSingleton(tracker) |> ignore) + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule(); + let! pipelineSummary = builder.ExecutePipelineAsync() |> Async.AwaitTask + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(tracker.CompletedCount()), chainDepth)) + + // Verify all chain modules executed + for i = 1 to chainDepth do + let record = tracker.GetRecord($"Chain{i}") + do! check(Assert.That(record.IsSome).IsTrue()) + + // Verify dependency ordering: each module must complete before its dependent + // Chain1 -> Chain2 -> Chain3 -> ... -> Chain50 + // Get all records sorted by completion order for diagnostics + let orderedRecords = + [ 1 .. chainDepth ] + |> List.choose (fun i -> tracker.GetRecord($"Chain{i}")) + |> List.sortBy (fun r -> r.CompletionOrder) + + // Verify the completion order matches the dependency order + for i, actualRecord in orderedRecords |> List.indexed do + let expectedModuleName = $"Chain{i + 1}" + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(actualRecord.ModuleName), + expectedModuleName + ).Because($"Module at completion order {i + 1} should be {expectedModuleName} but was {actualRecord.ModuleName}") + ) + } \ No newline at end of file From 3bceb53f9bc5d7e9d2e113e563d9903c6983aa2d Mon Sep 17 00:00:00 2001 From: James Alickolli Date: Wed, 13 May 2026 12:21:10 +1000 Subject: [PATCH 04/14] Update ScaleTests.fs --- .../ScaleTests.fs | 615 +++++++++++++++++- 1 file changed, 614 insertions(+), 1 deletion(-) diff --git a/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs b/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs index 3ace2fa4e0..aed4206b3c 100644 --- a/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs +++ b/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs @@ -805,6 +805,568 @@ module ScaleTestsModule = tracker.MarkCompleted("Chain50") return 50 } + + // Root module for fan-out + type FanOutRootModule(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutRoot") + tracker.MarkCompleted("FanOutRoot") + return true + } + + // Fan-out dependent modules - all depend on the root + [)>] + type FanOutDep1(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep1") + tracker.MarkCompleted("FanOutDep1") + return 1 + } + + [)>] + type FanOutDep2(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep2") + tracker.MarkCompleted("FanOutDep2") + return 2 + } + + [)>] + type FanOutDep3(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep3") + tracker.MarkCompleted("FanOutDep3") + return 3 + } + + [)>] + type FanOutDep4(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep4") + tracker.MarkCompleted("FanOutDep4") + return 4 + } + + [)>] + type FanOutDep5(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep5") + tracker.MarkCompleted("FanOutDep5") + return 5 + } + + [)>] + type FanOutDep6(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep6") + tracker.MarkCompleted("FanOutDep6") + return 6 + } + + [)>] + type FanOutDep7(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep7") + tracker.MarkCompleted("FanOutDep7") + return 7 + } + + [)>] + type FanOutDep8(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep8") + tracker.MarkCompleted("FanOutDep8") + return 8 + } + + [)>] + type FanOutDep9(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep9") + tracker.MarkCompleted("FanOutDep9") + return 9 + } + + [)>] + type FanOutDep10(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep10") + tracker.MarkCompleted("FanOutDep10") + return 10 + } + + [)>] + type FanOutDep11(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep11") + tracker.MarkCompleted("FanOutDep11") + return 11 + } + + [)>] + type FanOutDep12(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep12") + tracker.MarkCompleted("FanOutDep12") + return 12 + } + + [)>] + type FanOutDep13(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep13") + tracker.MarkCompleted("FanOutDep13") + return 13 + } + + [)>] + type FanOutDep14(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep14") + tracker.MarkCompleted("FanOutDep14") + return 14 + } + + [)>] + type FanOutDep15(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep15") + tracker.MarkCompleted("FanOutDep15") + return 15 + } + + [)>] + type FanOutDep16(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep16") + tracker.MarkCompleted("FanOutDep16") + return 16 + } + + [)>] + type FanOutDep17(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep17") + tracker.MarkCompleted("FanOutDep17") + return 17 + } + + [)>] + type FanOutDep18(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep18") + tracker.MarkCompleted("FanOutDep18") + return 18 + } + + [)>] + type FanOutDep19(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep19") + tracker.MarkCompleted("FanOutDep19") + return 19 + } + + [)>] + type FanOutDep20(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep20") + tracker.MarkCompleted("FanOutDep20") + return 20 + } + + [)>] + type FanOutDep21(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep21") + tracker.MarkCompleted("FanOutDep21") + return 21 + } + + [)>] + type FanOutDep22(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep22") + tracker.MarkCompleted("FanOutDep22") + return 22 + } + + [)>] + type FanOutDep23(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep23") + tracker.MarkCompleted("FanOutDep23") + return 23 + } + + [)>] + type FanOutDep24(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep24") + tracker.MarkCompleted("FanOutDep24") + return 24 + } + + [)>] + type FanOutDep25(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep25") + tracker.MarkCompleted("FanOutDep25") + return 25 + } + + [)>] + type FanOutDep26(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep26") + tracker.MarkCompleted("FanOutDep26") + return 26 + } + + [)>] + type FanOutDep27(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep27") + tracker.MarkCompleted("FanOutDep27") + return 27 + } + + [)>] + type FanOutDep28(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep28") + tracker.MarkCompleted("FanOutDep28") + return 28 + } + + [)>] + type FanOutDep29(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep29") + tracker.MarkCompleted("FanOutDep29") + return 29 + } + + [)>] + type FanOutDep30(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep30") + tracker.MarkCompleted("FanOutDep30") + return 30 + } + + [)>] + type FanOutDep31(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep31") + tracker.MarkCompleted("FanOutDep31") + return 31 + } + + [)>] + type FanOutDep32(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep32") + tracker.MarkCompleted("FanOutDep32") + return 32 + } + + [)>] + type FanOutDep33(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep33") + tracker.MarkCompleted("FanOutDep33") + return 33 + } + + [)>] + type FanOutDep34(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep34") + tracker.MarkCompleted("FanOutDep34") + return 34 + } + + [)>] + type FanOutDep35(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep35") + tracker.MarkCompleted("FanOutDep35") + return 35 + } + + [)>] + type FanOutDep36(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep36") + tracker.MarkCompleted("FanOutDep36") + return 36 + } + + [)>] + type FanOutDep37(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep37") + tracker.MarkCompleted("FanOutDep37") + return 37 + } + + [)>] + type FanOutDep38(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep38") + tracker.MarkCompleted("FanOutDep38") + return 38 + } + + [)>] + type FanOutDep39(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep39") + tracker.MarkCompleted("FanOutDep39") + return 39 + } + + [)>] + type FanOutDep40(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep40") + tracker.MarkCompleted("FanOutDep40") + return 40 + } + + [)>] + type FanOutDep41(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep41") + tracker.MarkCompleted("FanOutDep41") + return 41 + } + + [)>] + type FanOutDep42(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep42") + tracker.MarkCompleted("FanOutDep42") + return 42 + } + + [)>] + type FanOutDep43(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep43") + tracker.MarkCompleted("FanOutDep43") + return 43 + } + + [)>] + type FanOutDep44(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep44") + tracker.MarkCompleted("FanOutDep44") + return 44 + } + + [)>] + type FanOutDep45(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep45") + tracker.MarkCompleted("FanOutDep45") + return 45 + } + + [)>] + type FanOutDep46(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep46") + tracker.MarkCompleted("FanOutDep46") + return 46 + } + + [)>] + type FanOutDep47(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep47") + tracker.MarkCompleted("FanOutDep47") + return 47 + } + + [)>] + type FanOutDep48(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep48") + tracker.MarkCompleted("FanOutDep48") + return 48 + } + + [)>] + type FanOutDep49(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep49") + tracker.MarkCompleted("FanOutDep49") + return 49 + } + + [)>] + type FanOutDep50(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanOutDep50") + tracker.MarkCompleted("FanOutDep50") + return 50 + } /// /// Tests for large-scale pipeline scenarios to validate scalability, performance, and correct behavior with many @@ -939,4 +1501,55 @@ module ScaleTestsModule = expectedModuleName ).Because($"Module at completion order {i + 1} should be {expectedModuleName} but was {actualRecord.ModuleName}") ) - } \ No newline at end of file + } + + /// + /// Verifies that a fan-out pattern (1 root with 50 dependents) executes correctly. + /// + /// + /// This test validates: + /// - All 50 dependent modules wait for the root to complete + /// - Dependent modules can execute in parallel after the root completes + /// - No race conditions with many modules depending on one + /// + [] + member _.Pipeline_With1ModuleAnd50Dependents_CompletesSuccessfully() = + async { + let tracker = new ExecutionTracker() + do! check(Assert.That(tracker.IsClean).IsTrue()) + let totalModules = 51 + + let builder = + TestPipelineHostBuilder.Create() + .ConfigureServices(fun _ services -> services.AddSingleton(tracker) |> ignore) + .AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule() + + let! pipelineSummary = builder.ExecutePipelineAsync() |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(tracker.CompletedCount()), totalModules)) + + let rootRecord = tracker.GetRecord("FanOutRoot") + do! check(Assert.That(rootRecord.IsSome).IsTrue()) + + for i = 1 to 50 do + let dependent = tracker.GetRecord($"FanOutDep{i}") + do! check(Assert.That(dependent.IsSome).IsTrue()) + } \ No newline at end of file From ec8e97c31c30ce14ebdd29f9abeda34c43f151aa Mon Sep 17 00:00:00 2001 From: James Alickolli Date: Wed, 13 May 2026 12:28:29 +1000 Subject: [PATCH 05/14] completed scale tests for fsharp --- .../ScaleTests.fs | 613 ++++++++++++++++++ 1 file changed, 613 insertions(+) diff --git a/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs b/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs index aed4206b3c..2db734cc27 100644 --- a/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs +++ b/test/ModularPipelines.UnitTests.FSharp/ScaleTests.fs @@ -1368,6 +1368,568 @@ module ScaleTestsModule = return 50 } + // Independent modules for fan-in + type FanInInd1(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd1") + tracker.MarkCompleted("FanInInd1") + return 1 + } + + type FanInInd2(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd2") + tracker.MarkCompleted("FanInInd2") + return 2 + } + + type FanInInd3(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd3") + tracker.MarkCompleted("FanInInd3") + return 3 + } + + type FanInInd4(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd4") + tracker.MarkCompleted("FanInInd4") + return 4 + } + + type FanInInd5(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd5") + tracker.MarkCompleted("FanInInd5") + return 5 + } + + type FanInInd6(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd6") + tracker.MarkCompleted("FanInInd6") + return 6 + } + + type FanInInd7(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd7") + tracker.MarkCompleted("FanInInd7") + return 7 + } + + type FanInInd8(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd8") + tracker.MarkCompleted("FanInInd8") + return 8 + } + + type FanInInd9(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd9") + tracker.MarkCompleted("FanInInd9") + return 9 + } + + type FanInInd10(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd10") + tracker.MarkCompleted("FanInInd10") + return 10 + } + + type FanInInd11(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd11") + tracker.MarkCompleted("FanInInd11") + return 11 + } + + type FanInInd12(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd12") + tracker.MarkCompleted("FanInInd12") + return 12 + } + + type FanInInd13(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd13") + tracker.MarkCompleted("FanInInd13") + return 13 + } + + type FanInInd14(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd14") + tracker.MarkCompleted("FanInInd14") + return 14 + } + + type FanInInd15(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd15") + tracker.MarkCompleted("FanInInd15") + return 15 + } + + type FanInInd16(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd16") + tracker.MarkCompleted("FanInInd16") + return 16 + } + + type FanInInd17(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd17") + tracker.MarkCompleted("FanInInd17") + return 17 + } + + type FanInInd18(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd18") + tracker.MarkCompleted("FanInInd18") + return 18 + } + + type FanInInd19(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd19") + tracker.MarkCompleted("FanInInd19") + return 19 + } + + type FanInInd20(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd20") + tracker.MarkCompleted("FanInInd20") + return 20 + } + + type FanInInd21(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd21") + tracker.MarkCompleted("FanInInd21") + return 21 + } + + type FanInInd22(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd22") + tracker.MarkCompleted("FanInInd22") + return 22 + } + + type FanInInd23(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd23") + tracker.MarkCompleted("FanInInd23") + return 23 + } + + type FanInInd24(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd24") + tracker.MarkCompleted("FanInInd24") + return 24 + } + + type FanInInd25(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd25") + tracker.MarkCompleted("FanInInd25") + return 25 + } + + type FanInInd26(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd26") + tracker.MarkCompleted("FanInInd26") + return 26 + } + + type FanInInd27(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd27") + tracker.MarkCompleted("FanInInd27") + return 27 + } + + type FanInInd28(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd28") + tracker.MarkCompleted("FanInInd28") + return 28 + } + + type FanInInd29(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd29") + tracker.MarkCompleted("FanInInd29") + return 29 + } + + type FanInInd30(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd30") + tracker.MarkCompleted("FanInInd30") + return 30 + } + + type FanInInd31(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd31") + tracker.MarkCompleted("FanInInd31") + return 31 + } + + type FanInInd32(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd32") + tracker.MarkCompleted("FanInInd32") + return 32 + } + + type FanInInd33(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd33") + tracker.MarkCompleted("FanInInd33") + return 33 + } + + type FanInInd34(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd34") + tracker.MarkCompleted("FanInInd34") + return 34 + } + + type FanInInd35(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd35") + tracker.MarkCompleted("FanInInd35") + return 35 + } + + type FanInInd36(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd36") + tracker.MarkCompleted("FanInInd36") + return 36 + } + + type FanInInd37(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd37") + tracker.MarkCompleted("FanInInd37") + return 37 + } + + type FanInInd38(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd38") + tracker.MarkCompleted("FanInInd38") + return 38 + } + + type FanInInd39(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd39") + tracker.MarkCompleted("FanInInd39") + return 39 + } + + type FanInInd40(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd40") + tracker.MarkCompleted("FanInInd40") + return 40 + } + + type FanInInd41(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd41") + tracker.MarkCompleted("FanInInd41") + return 41 + } + + type FanInInd42(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd42") + tracker.MarkCompleted("FanInInd42") + return 42 + } + + type FanInInd43(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd43") + tracker.MarkCompleted("FanInInd43") + return 43 + } + + type FanInInd44(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd44") + tracker.MarkCompleted("FanInInd44") + return 44 + } + + type FanInInd45(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd45") + tracker.MarkCompleted("FanInInd45") + return 45 + } + + type FanInInd46(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd46") + tracker.MarkCompleted("FanInInd46") + return 46 + } + + type FanInInd47(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd47") + tracker.MarkCompleted("FanInInd47") + return 47 + } + + type FanInInd48(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd48") + tracker.MarkCompleted("FanInInd48") + return 48 + } + + type FanInInd49(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd49") + tracker.MarkCompleted("FanInInd49") + return 49 + } + + type FanInInd50(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInInd50") + tracker.MarkCompleted("FanInInd50") + return 50 + } + + // Final module that depends on all independent modules + [); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof); + ModularPipelines.Attributes.DependsOn(typeof)>] + type FanInFinalModule(tracker: ExecutionTracker) = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken: CancellationToken) = + task { + tracker.RecordStart("FanInFinal") + tracker.MarkCompleted("FanInFinal") + return true + } + /// /// Tests for large-scale pipeline scenarios to validate scalability, performance, and correct behavior with many /// modules. @@ -1552,4 +2114,55 @@ module ScaleTestsModule = for i = 1 to 50 do let dependent = tracker.GetRecord($"FanOutDep{i}") do! check(Assert.That(dependent.IsSome).IsTrue()) + } + + /// + /// Verifies that a fan-in pattern (50 independent modules + 1 final) executes correctly. + /// + /// + /// This test validates: + /// - The final module waits for all 50 independent modules to complete + /// - Independent modules can execute in parallel + /// - No race conditions with one module depending on many + /// + [] + member _.Pipeline_With50ModulesAndOneFinalModule_CompletesSuccessfully() = + async { + let tracker = new ExecutionTracker() + do! check(Assert.That(tracker.IsClean).IsTrue()) + let totalModules = 51 + + let builder = + TestPipelineHostBuilder.Create() + .ConfigureServices(fun _ services -> services.AddSingleton(tracker) |> ignore) + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule().AddModule() + .AddModule().AddModule() + .AddModule() + + let! pipelineSummary = builder.ExecutePipelineAsync() |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(tracker.CompletedCount()), totalModules)) + + let finalRecord = tracker.GetRecord("FanInFinal") + do! check(Assert.That(finalRecord.IsSome).IsTrue()) + + for i = 1 to 50 do + let independent = tracker.GetRecord($"FanInInd{i}") + do! check(Assert.That(independent.IsSome).IsTrue()) } \ No newline at end of file From db359b0ddf2579aa3aff1c8d9dc6023eb814a40f Mon Sep 17 00:00:00 2001 From: James Alickolli Date: Wed, 13 May 2026 13:12:03 +1000 Subject: [PATCH 06/14] Created FlexibleDependencyApiExportTests --- .../Api/FlexibleDependencyApiExportTests.fs | 238 ++++++++++++++++++ .../ModularPipelines.UnitTests.FSharp.fsproj | 1 + 2 files changed, 239 insertions(+) create mode 100644 test/ModularPipelines.UnitTests.FSharp/Api/FlexibleDependencyApiExportTests.fs diff --git a/test/ModularPipelines.UnitTests.FSharp/Api/FlexibleDependencyApiExportTests.fs b/test/ModularPipelines.UnitTests.FSharp/Api/FlexibleDependencyApiExportTests.fs new file mode 100644 index 0000000000..071fce88d1 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Api/FlexibleDependencyApiExportTests.fs @@ -0,0 +1,238 @@ +namespace ModularPipelines.UnitTests.FSharp.Api + +open System +open ModularPipelines.Attributes +open ModularPipelines.Context +open ModularPipelines.DependencyInjection +open ModularPipelines.Modules +open TUnit.Core +open TUnit.Assertions.FSharp.Operations +open TUnit.Assertions.Extensions +open TUnit.Assertions + +/// +/// Verifies that all public types from the flexible dependency API are accessible from their expected namespaces. This +/// ensures the API surface is correctly exported and consumable by library users. +/// +type FlexibleDependencyApiExportTests() = + [] + member _.IDependencyContext_IsAccessibleFromContextNamespace() = + async { + let dependencyContextType = typeof + + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(dependencyContextType.Namespace), + "ModularPipelines.Context" + ) + ) + + do! check(Assert.That(dependencyContextType.IsPublic).IsTrue()) + do! check(Assert.That(dependencyContextType.IsInterface).IsTrue()) + } + [] + member _.DependsOnBaseAttribute_IsAccessibleFromAttributesNamespace() = async { + // Verify DependsOnBaseAttribute is in ModularPipelines.Attributes namespace + let dependsOnBaseAttributeType = typeof + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(dependsOnBaseAttributeType.Namespace), + "ModularPipelines.Attributes" + ) + ) + do! check(Assert.That(dependsOnBaseAttributeType.IsPublic).IsTrue()) + do! check(Assert.That(dependsOnBaseAttributeType.IsAbstract).IsTrue()) + do! check(Assert.That(dependsOnBaseAttributeType.IsSubclassOf(typeof)).IsTrue()) + } + + [] + member _.ModuleTagAttribute_IsAccessibleFromAttributesNamespace() = async { + // Verify ModuleTagAttribute is in ModularPipelines.Attributes namespace + let moduleTagAttributeType = typeof + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(moduleTagAttributeType.Namespace), + "ModularPipelines.Attributes" + ) + ) + do! check(Assert.That(moduleTagAttributeType.IsPublic).IsTrue()) + do! check(Assert.That(moduleTagAttributeType.IsSealed).IsTrue()) + do! check(Assert.That(moduleTagAttributeType.IsSubclassOf(typeof)).IsTrue()) + } + + [] + member _.ModuleCategoryAttribute_IsAccessibleFromAttributesNamespace() = async { + // Verify ModuleCategoryAttribute is in ModularPipelines.Attributes namespace + let moduleCategoryAttributeType = typeof + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(moduleCategoryAttributeType.Namespace), + "ModularPipelines.Attributes" + ) + ) + do! check(Assert.That(moduleCategoryAttributeType.IsPublic).IsTrue()) + do! check(Assert.That(moduleCategoryAttributeType.IsSealed).IsTrue()) + do! check(Assert.That(moduleCategoryAttributeType.IsSubclassOf(typeof)).IsTrue()) + } + + [] + member _.DependsOnModulesWithTagAttribute_IsAccessibleFromAttributesNamespace() = async { + // Verify DependsOnModulesWithTagAttribute is in ModularPipelines.Attributes namespace + let dependsOnModulesWithTagAttributeType = typeof + + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(dependsOnModulesWithTagAttributeType.Namespace), + "ModularPipelines.Attributes" + ) + ) + do! check(Assert.That(dependsOnModulesWithTagAttributeType.IsPublic).IsTrue()) + do! check(Assert.That(dependsOnModulesWithTagAttributeType.IsSealed).IsTrue()) + do! check(Assert.That(dependsOnModulesWithTagAttributeType.IsSubclassOf(typeof)).IsTrue()) + } + + [] + member _.DependsOnModulesInCategoryAttribute_IsAccessibleFromAttributesNamespace() = async { + // Verify DependsOnModulesInCategoryAttribute is in ModularPipelines.Attributes namespace + let dependsOnModulesInCategoryAttributeType = typeof + + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(dependsOnModulesInCategoryAttributeType.Namespace), + "ModularPipelines.Attributes" + ) + ) + do! check(Assert.That(dependsOnModulesInCategoryAttributeType.IsPublic).IsTrue()) + do! check(Assert.That(dependsOnModulesInCategoryAttributeType.IsSealed).IsTrue()) + do! check(Assert.That(dependsOnModulesInCategoryAttributeType.IsSubclassOf(typeof)).IsTrue()) + } + + [] + member _.DependsOnModulesWithAttributeAttribute_IsAccessibleFromAttributesNamespace() = async { + // Verify DependsOnModulesWithAttributeAttribute is in ModularPipelines.Attributes namespace + let dependsOnModulesWithAttributeAttributeType = typedefof> + + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(dependsOnModulesWithAttributeAttributeType.Namespace), + "ModularPipelines.Attributes" + ) + ) + do! check(Assert.That(dependsOnModulesWithAttributeAttributeType.IsPublic).IsTrue()) + do! check(Assert.That(dependsOnModulesWithAttributeAttributeType.IsGenericTypeDefinition).IsTrue()) + + let closedType = typeof> + do! check(Assert.That(closedType.IsSubclassOf(typeof)).IsTrue()) + } + + [] + member _.ITaggedModule_IsAccessibleFromModulesNamespace() = async { + // Verify ITaggedModule is in ModularPipelines.Modules namespace + let taggedModuleType = typeof + + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(taggedModuleType.Namespace), + "ModularPipelines.Modules" + ) + ) + do! check(Assert.That(taggedModuleType.IsPublic).IsTrue()) + do! check(Assert.That(taggedModuleType.IsInterface).IsTrue()) + } + + [] + member _.IModuleRegistrationBuilder_IsAccessibleFromDependencyInjectionNamespace() = async { + // Verify IModuleRegistrationBuilder is in ModularPipelines.DependencyInjection namespace + let moduleRegistrationBuilderType = typeof + + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(moduleRegistrationBuilderType.Namespace), + "ModularPipelines.DependencyInjection" + ) + ) + do! check(Assert.That(moduleRegistrationBuilderType.IsPublic).IsTrue()) + do! check(Assert.That(moduleRegistrationBuilderType.IsInterface).IsTrue()) + } + + [] + member _.AllFlexibleDependencyAttributes_HaveCorrectAttributeUsage() = async { + // Verify all dependency attributes allow multiple usage and inheritance + let dependencyAttributes = + [| + typeof + typeof + typeof + typeof> + |] + + for attrType in dependencyAttributes do + let usage = + attrType.GetCustomAttributes(typeof, false) + |> Array.choose (function + | :? AttributeUsageAttribute as attributeUsage -> Some attributeUsage + | _ -> None) + |> Array.tryHead + + do! check(Assert.That(usage.IsSome).IsTrue()) + + match usage with + | Some attributeUsage -> + do! check(Assert.That(attributeUsage.AllowMultiple).IsTrue()) + do! check(Assert.That(attributeUsage.Inherited).IsTrue()) + | None -> () + } + + [] + member _.ModuleCategoryAttribute_DoesNotAllowMultiple() = async { + // Verify ModuleCategoryAttribute does NOT allow multiple (only one category per module) + let usage = + typeof.GetCustomAttributes(typeof, false) + |> Array.choose (function + | :? AttributeUsageAttribute as attributeUsage -> Some attributeUsage + | _ -> None) + |> Array.tryHead + + do! check(Assert.That(usage.IsSome).IsTrue()) + + match usage with + | Some attributeUsage -> + do! check(Assert.That(attributeUsage.AllowMultiple).IsFalse()) + do! check(Assert.That(attributeUsage.Inherited).IsTrue()) + | None -> () + } + + [] + member _.IDependencyContext_HasExpectedMethods() = async { + // Verify IDependencyContext has all required methods for dependency resolution + let dependencyContextType = typeof + let methods = dependencyContextType.GetMethods() + + do! check(Assert.That(methods |> Array.exists (fun m -> m.Name = "GetTags")).IsTrue()) + do! check(Assert.That(methods |> Array.exists (fun m -> m.Name = "GetCategory")).IsTrue()) + do! check(Assert.That(methods |> Array.exists (fun m -> m.Name = "HasAttribute")).IsTrue()) + do! check(Assert.That(methods |> Array.exists (fun m -> m.Name = "GetAttribute")).IsTrue()) + do! check(Assert.That(methods |> Array.exists (fun m -> m.Name = "GetAttributes")).IsTrue()) + } + + [] + member _.ITaggedModule_HasExpectedProperties() = async { + // Verify ITaggedModule has all required properties + let taggedModuleType = typeof + let properties = taggedModuleType.GetProperties() + + do! check(Assert.That(properties |> Array.exists (fun p -> p.Name = "Tags")).IsTrue()) + do! check(Assert.That(properties |> Array.exists (fun p -> p.Name = "Category")).IsTrue()) + } + + [] + member _.IModuleRegistrationBuilder_HasExpectedMembers() = async { + // Verify IModuleRegistrationBuilder has all required members + let moduleRegistrationBuilderType = typeof + let properties = moduleRegistrationBuilderType.GetProperties() + let methods = moduleRegistrationBuilderType.GetMethods() + + do! check(Assert.That(properties |> Array.exists (fun p -> p.Name = "Services")).IsTrue()) + do! check(Assert.That(methods |> Array.exists (fun m -> m.Name = "WithTags")).IsTrue()) + do! check(Assert.That(methods |> Array.exists (fun m -> m.Name = "WithCategory")).IsTrue()) + } \ No newline at end of file diff --git a/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj b/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj index 1e0a998c81..7e37acb029 100644 --- a/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj +++ b/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj @@ -28,6 +28,7 @@ + From c20be0c47195bf4aa16fe4b3567a780d279909ef Mon Sep 17 00:00:00 2001 From: James Alickolli Date: Wed, 13 May 2026 16:18:26 +1000 Subject: [PATCH 07/14] Added AttributeEventInvokerTests --- src/ModularPipelines/ModularPipelines.csproj | 3 + .../Attributes/AttributeEventInvokerTests.fs | 84 +++++++++++++++++++ .../ModularPipelines.UnitTests.FSharp.fsproj | 3 +- 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 test/ModularPipelines.UnitTests.FSharp/Attributes/AttributeEventInvokerTests.fs diff --git a/src/ModularPipelines/ModularPipelines.csproj b/src/ModularPipelines/ModularPipelines.csproj index 0b60f8cc34..880b95368d 100644 --- a/src/ModularPipelines/ModularPipelines.csproj +++ b/src/ModularPipelines/ModularPipelines.csproj @@ -40,6 +40,9 @@ <_Parameter1>ModularPipelines.UnitTests + + <_Parameter1>ModularPipelines.UnitTests.FSharp + <_Parameter1>ModularPipelines.TestHelpers diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/AttributeEventInvokerTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/AttributeEventInvokerTests.fs new file mode 100644 index 0000000000..0b8f952c78 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/AttributeEventInvokerTests.fs @@ -0,0 +1,84 @@ +namespace ModularPipelines.UnitTests.Attributes + +open Microsoft.Extensions.Logging +open ModularPipelines.Attributes.Events +open ModularPipelines.Context +open ModularPipelines.Engine.Attributes +open Moq +open System +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module AttributeEventInvokerTests = + type private SuccessfulHandler() = + member val WasCalled = false with get, private set + interface IModuleStartHandler with + member _.ContinueOnError = false + member this.OnModuleStartAsync(_: ModularPipelines.Context.IModuleHookContext): System.Threading.Tasks.Task = + task { + this.WasCalled <- true + } + + type private FailingHandler() = + interface IModuleStartHandler with + member _.ContinueOnError = false + member _.OnModuleStartAsync(_: ModularPipelines.Context.IModuleHookContext): System.Threading.Tasks.Task = + raise (InvalidOperationException("Test exception")) + + type private FailingHandlerWithContinue() = + interface IModuleStartHandler with + member _.ContinueOnError = true + member _.OnModuleStartAsync(_: ModularPipelines.Context.IModuleHookContext): System.Threading.Tasks.Task = + raise (InvalidOperationException("Test exception")) + + type AttributeEventInvokerTests() = + [] + member _.InvokeAsync_CallsAllHandlers() = async { + let handler1 = SuccessfulHandler() + let handler2 = SuccessfulHandler() + let handlers = [ handler1 :> IModuleStartHandler; handler2 :> IModuleStartHandler ] + let invoker = AttributeEventInvoker(Mock.Of>()) + let context = Mock.Of() + + do! invoker.InvokeStartHandlersAsync(handlers, context) |> Async.AwaitTask + + do! check(Assert.That(handler1.WasCalled).IsTrue()) + do! check(Assert.That(handler2.WasCalled).IsTrue()) + } + + [] + member _.InvokeAsync_HandlerThrows_ContinueOnErrorFalse_Propagates() = async { + let handler = FailingHandler() + let handlers = [ handler :> IModuleStartHandler ] + let invoker = AttributeEventInvoker(Mock.Of>()) + let context = Mock.Of() + + let mutable thrownException = None + + try + do! invoker.InvokeStartHandlersAsync(handlers, context) |> Async.AwaitTask + with ex -> + thrownException <- Some ex + + do! check(Assert.That(thrownException.IsSome).IsTrue()) + + match thrownException with + | Some ex -> + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(ex.GetBaseException().Message), "Test exception")) + | None -> () + } + + [] + member _.InvokeAsync_HandlerThrows_ContinueOnErrorTrue_Continues() = async { + let failingHandler = FailingHandlerWithContinue() + let successHandler = SuccessfulHandler() + let handlers = [ failingHandler :> IModuleStartHandler; successHandler :> IModuleStartHandler ] + let invoker = AttributeEventInvoker(Mock.Of>()) + let context = Mock.Of() + + do! invoker.InvokeStartHandlersAsync(handlers, context) |> Async.AwaitTask + + do! check(Assert.That(successHandler.WasCalled).IsTrue()) + } \ No newline at end of file diff --git a/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj b/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj index 7e37acb029..53301dd348 100644 --- a/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj +++ b/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj @@ -28,12 +28,13 @@ - + + From 7f148beaef44cf09851421b1d7ccf0bb69d76740 Mon Sep 17 00:00:00 2001 From: James Alickolli Date: Wed, 13 May 2026 17:20:25 +1000 Subject: [PATCH 08/14] Created CliAttributeTests.fs --- .../Attributes/CliAttributeTests.fs | 21 +++++++++++++++++++ .../ModularPipelines.UnitTests.FSharp.fsproj | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 test/ModularPipelines.UnitTests.FSharp/Attributes/CliAttributeTests.fs diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/CliAttributeTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/CliAttributeTests.fs new file mode 100644 index 0000000000..3a46f6ad99 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/CliAttributeTests.fs @@ -0,0 +1,21 @@ +namespace ModularPipelines.UnitTests.Attributes +open ModularPipelines.Helpers.Internal +open TUnit.Core +open ModularPipelines.Attributes +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations + +type CliAttributeTests = + member private this.ModelProvider = CommandModelProvider() + member private this.ArgumentBuilder = CommandArgumentBuilder() + member private this.BuildArguments(optionsObject: obj) = + let model = this.ModelProvider.GetCommandModel(optionsObject.GetType()) + this.ArgumentBuilder.BuildArguments(model, optionsObject) + + [] + member _.CliCommand_Returns_Tool_And_SubCommands() = async { + let attribute = new CliCommandAttribute("helm", "install"); + let parts: string array = attribute.GetAllParts(); + do! check(Assert.That(parts).IsEquivalentTo([| "helm"; "install" |])) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj b/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj index 53301dd348..b00939dd2a 100644 --- a/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj +++ b/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj @@ -33,7 +33,7 @@ - + From 2a4ceaf0b7ecfe85c127c1a87024b857fa0a1c05 Mon Sep 17 00:00:00 2001 From: James Alickolli Date: Wed, 13 May 2026 19:56:44 +1000 Subject: [PATCH 09/14] Update CliAttributeTests.fs --- .../Attributes/CliAttributeTests.fs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/CliAttributeTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/CliAttributeTests.fs index 3a46f6ad99..8ca49de255 100644 --- a/test/ModularPipelines.UnitTests.FSharp/Attributes/CliAttributeTests.fs +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/CliAttributeTests.fs @@ -6,7 +6,7 @@ open TUnit.Assertions open TUnit.Assertions.Extensions open TUnit.Assertions.FSharp.Operations -type CliAttributeTests = +type CliAttributeTests() = member private this.ModelProvider = CommandModelProvider() member private this.ArgumentBuilder = CommandArgumentBuilder() member private this.BuildArguments(optionsObject: obj) = @@ -19,3 +19,26 @@ type CliAttributeTests = let parts: string array = attribute.GetAllParts(); do! check(Assert.That(parts).IsEquivalentTo([| "helm"; "install" |])) } + + [] + member _.CliCommand_Returns_Only_Tool_When_No_SubCommands() = async { + let attribute = new CliCommandAttribute("helm"); + let parts: string array = attribute.GetAllParts(); + do! check(Assert.That(parts).IsEquivalentTo([| "helm" |])) + } + + [] + member _.CliCommand_Returns_Multiple_SubCommands() = async { + let attribute = new CliCommandAttribute("kubectl", "get", "pods"); + let parts: string array = attribute.GetAllParts(); + do! check(Assert.That(parts).IsEquivalentTo([| "kubectl"; "get"; "pods" |])) + } + + [] + member _.CliFlag_Returns_Name_When_ShortForm_Not_Preferred() = async { + let attribute = CliFlagAttribute("--debug") + attribute.ShortForm <- "-d" + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(attribute.GetEffectiveName()), "--debug")) + } + + \ No newline at end of file From 41b243c2395f37e0835ad937d4aa6548b3043aa1 Mon Sep 17 00:00:00 2001 From: James Alickolli Date: Wed, 13 May 2026 20:02:15 +1000 Subject: [PATCH 10/14] Update CliAttributeTests.fs --- .../Attributes/CliAttributeTests.fs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/CliAttributeTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/CliAttributeTests.fs index 8ca49de255..d1abcd16b5 100644 --- a/test/ModularPipelines.UnitTests.FSharp/Attributes/CliAttributeTests.fs +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/CliAttributeTests.fs @@ -1,4 +1,5 @@ namespace ModularPipelines.UnitTests.Attributes + open ModularPipelines.Helpers.Internal open TUnit.Core open ModularPipelines.Attributes @@ -6,6 +7,93 @@ open TUnit.Assertions open TUnit.Assertions.Extensions open TUnit.Assertions.FSharp.Operations +[] +type private TestCliOptionsWithFlag = + { + [] + Debug: System.Nullable + } + +[] +type private TestCliOptionsWithOption = + { + [] + Namespace: string + } + +[] +type private TestCliOptionsWithEqualsSeparator = + { + [] + Set: string + } + +[] +type private TestCliOptionsWithMultipleValues = + { + [] + Values: string array + } + +[] +type private TestCliOptionsWithArgumentAfterOptions = + { + [] + ReleaseName: string + + [] + Debug: System.Nullable + } + +[] +type private TestCliOptionsWithArgumentBeforeOptions = + { + [] + Path: string + + [] + Debug: System.Nullable + } + +[] +type private TestCliOptionsWithOptionalArgument = + { + [] + ReleaseName: string + + [] + Debug: System.Nullable + } + +[] +type private TestCliOptionsWithMultipleArguments = + { + [] + ReleaseName: string + + [] + ChartReference: string + } + +[] +type private TestCliOptionsComplete = + { + [] + ReleaseName: string + + [] + ChartReference: string + + [] + Debug: System.Nullable + + [] + Namespace: string + + [] + Set: string array + } + type CliAttributeTests() = member private this.ModelProvider = CommandModelProvider() member private this.ArgumentBuilder = CommandArgumentBuilder() From 75e3d767a5f4a0bdd7c7e6d9564cfa32ca989d6e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 23:37:49 +1000 Subject: [PATCH 11/14] =?UTF-8?q?Convert=20C#=20unit=20tests=20to=20F#=20(?= =?UTF-8?q?partial=20=E2=80=94=20Attributes,=20Models,=20State,=20Validati?= =?UTF-8?q?on,=20Requirements,=20Results,=20Modules)=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Convert C# Attributes unit tests to F# Add F# equivalents for 13 test files in the Attributes folder: - CliToolAttributeTests.fs - DotNetFormatOptionsTests.fs - DynamicDependencyIntegrationTests.fs - EnumValueAttributeTests.fs - LifecycleEventIntegrationTests.fs - LinuxOnlyTestAttribute.fs - MetadataCrossPhaseIntegrationTests.fs - ModuleAttributeEventServiceTests.fs - ModuleDependencyRegistryTests.fs - ModuleMetadataRegistryTests.fs - ModuleReadyEventTests.fs - ModuleRegistrationContextTests.fs - WindowsOnlyTestAttribute.fs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * Add F# unit test conversions for Models, State, Validation, Requirements, Results, Modules Convert 18 C# unit test files to idiomatic F# equivalents in test/ModularPipelines.UnitTests.FSharp/: - Models/: MyModel.fs, CommandLineTests.fs, JsonSerializationTests.fs, KeyValueTests.fs, RequirementDecisionTests.fs, SkipDecisionTests.fs, TrxParsingTests.fs - State/: ModuleExecutionPhaseTests.fs, ModuleStateStoreTests.fs, ModuleStateTransitionsTests.fs - Validation/: ValidationInterfaceTests.fs, ValidationTests.fs - Requirements/: PipelineRequirementBaseClassTests.fs, RequireFactoryTests.fs - Results/: ResultsRepositoryTests.fs, ReturnNothingTests.fs - Modules/: SyncModuleTests.fs, TestModule1.fs - Updated .fsproj with wildcard compile entries for all 6 new directories Note: Build has remaining errors to fix (assertion extension methods, ModuleStateSnapshot required properties). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * fix F# unit test build regressions Agent-Logs-Url: https://github.com/licon4812/ModularPipelines/sessions/7e44fb1f-68fb-4d1a-93a9-59d8e808e306 Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * address F# test review follow-ups Agent-Logs-Url: https://github.com/licon4812/ModularPipelines/sessions/7e44fb1f-68fb-4d1a-93a9-59d8e808e306 Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * tighten F# state test cleanup Agent-Logs-Url: https://github.com/licon4812/ModularPipelines/sessions/7e44fb1f-68fb-4d1a-93a9-59d8e808e306 Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * finalize F# event handler cleanup Agent-Logs-Url: https://github.com/licon4812/ModularPipelines/sessions/7e44fb1f-68fb-4d1a-93a9-59d8e808e306 Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * fix remaining F# attribute test build errors Agent-Logs-Url: https://github.com/licon4812/ModularPipelines/sessions/c9451e79-96d6-4d20-91e7-5a00f713e05c Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * normalize F# metadata event formatting Agent-Logs-Url: https://github.com/licon4812/ModularPipelines/sessions/c9451e79-96d6-4d20-91e7-5a00f713e05c Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * Update ResultsRepositoryTests.fs * complete F# CliAttributeTests conversion Agent-Logs-Url: https://github.com/licon4812/ModularPipelines/sessions/d613e767-c418-4b8e-b79b-44c5ebf97bcf Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * guard mixed cli attribute ordering assertion Agent-Logs-Url: https://github.com/licon4812/ModularPipelines/sessions/d613e767-c418-4b8e-b79b-44c5ebf97bcf Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * avoid unsafe head access in cli attribute test Agent-Logs-Url: https://github.com/licon4812/ModularPipelines/sessions/d613e767-c418-4b8e-b79b-44c5ebf97bcf Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * Fixed FSharp Attribute tests namespace * finish F# state and validation test conversions Agent-Logs-Url: https://github.com/licon4812/ModularPipelines/sessions/34b9a83f-ebc5-4758-bdda-fe0ee0581f30 Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * return non-null result in self-referencing validation module Agent-Logs-Url: https://github.com/licon4812/ModularPipelines/sessions/34b9a83f-ebc5-4758-bdda-fe0ee0581f30 Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> --------- Co-authored-by: Copilot Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: James Alickolli --- .../Attributes/AttributeEventInvokerTests.fs | 2 +- .../Attributes/CliAttributeTests.fs | 160 +++++- .../Attributes/CliToolAttributeTests.fs | 69 +++ .../Attributes/DotNetFormatOptionsTests.fs | 28 + .../DynamicDependencyIntegrationTests.fs | 65 +++ .../Attributes/EnumValueAttributeTests.fs | 40 ++ .../LifecycleEventIntegrationTests.fs | 132 +++++ .../Attributes/LinuxOnlyTestAttribute.fs | 10 + .../MetadataCrossPhaseIntegrationTests.fs | 80 +++ .../ModuleAttributeEventServiceTests.fs | 141 ++++++ .../ModuleDependencyRegistryTests.fs | 63 +++ .../Attributes/ModuleMetadataRegistryTests.fs | 47 ++ .../Attributes/ModuleReadyEventTests.fs | 168 ++++++ .../ModuleRegistrationContextTests.fs | 89 ++++ .../Attributes/WindowsOnlyTestAttribute.fs | 10 + .../Models/CommandLineTests.fs | 36 ++ .../Models/JsonSerializationTests.fs | 56 ++ .../Models/KeyValueTests.fs | 31 ++ .../Models/MyModel.fs | 9 + .../Models/RequirementDecisionTests.fs | 57 +++ .../Models/SkipDecisionTests.fs | 57 +++ .../Models/TrxParsingTests.fs | 102 ++++ .../ModularPipelines.UnitTests.FSharp.fsproj | 10 +- .../Modules/SyncModuleTests.fs | 319 ++++++++++++ .../Modules/TestModule1.fs | 8 + .../PipelineRequirementBaseClassTests.fs | 202 ++++++++ .../Requirements/RequireFactoryTests.fs | 220 ++++++++ .../Results/ResultsRepositoryTests.fs | 107 ++++ .../Results/ReturnNothingTests.fs | 51 ++ .../State/ModuleExecutionPhaseTests.fs | 169 ++++++ .../State/ModuleStateStoreTests.fs | 479 ++++++++++++++++++ .../State/ModuleStateTransitionsTests.fs | 367 ++++++++++++++ .../Validation/ValidationInterfaceTests.fs | 36 ++ .../Validation/ValidationTests.fs | 322 ++++++++++++ 34 files changed, 3737 insertions(+), 5 deletions(-) create mode 100644 test/ModularPipelines.UnitTests.FSharp/Attributes/CliToolAttributeTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Attributes/DotNetFormatOptionsTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Attributes/DynamicDependencyIntegrationTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Attributes/EnumValueAttributeTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Attributes/LifecycleEventIntegrationTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Attributes/LinuxOnlyTestAttribute.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Attributes/MetadataCrossPhaseIntegrationTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleAttributeEventServiceTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleDependencyRegistryTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleMetadataRegistryTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleReadyEventTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleRegistrationContextTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Attributes/WindowsOnlyTestAttribute.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Models/CommandLineTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Models/JsonSerializationTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Models/KeyValueTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Models/MyModel.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Models/RequirementDecisionTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Models/SkipDecisionTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Models/TrxParsingTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Modules/SyncModuleTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Modules/TestModule1.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Requirements/PipelineRequirementBaseClassTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Requirements/RequireFactoryTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Results/ResultsRepositoryTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Results/ReturnNothingTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/State/ModuleExecutionPhaseTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/State/ModuleStateStoreTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/State/ModuleStateTransitionsTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Validation/ValidationInterfaceTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Validation/ValidationTests.fs diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/AttributeEventInvokerTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/AttributeEventInvokerTests.fs index 0b8f952c78..6d5cfbf44d 100644 --- a/test/ModularPipelines.UnitTests.FSharp/Attributes/AttributeEventInvokerTests.fs +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/AttributeEventInvokerTests.fs @@ -1,4 +1,4 @@ -namespace ModularPipelines.UnitTests.Attributes +namespace ModularPipelines.UnitTests.FSharp.Attributes open Microsoft.Extensions.Logging open ModularPipelines.Attributes.Events diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/CliAttributeTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/CliAttributeTests.fs index d1abcd16b5..ead88e1824 100644 --- a/test/ModularPipelines.UnitTests.FSharp/Attributes/CliAttributeTests.fs +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/CliAttributeTests.fs @@ -1,4 +1,4 @@ -namespace ModularPipelines.UnitTests.Attributes +namespace ModularPipelines.UnitTests.FSharp.Attributes open ModularPipelines.Helpers.Internal open TUnit.Core @@ -129,4 +129,160 @@ type CliAttributeTests() = do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(attribute.GetEffectiveName()), "--debug")) } - \ No newline at end of file + [] + member _.CliFlag_Returns_ShortForm_When_Preferred() = async { + let attribute = CliFlagAttribute("--debug") + attribute.ShortForm <- "-d" + attribute.PreferShortForm <- true + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(attribute.GetEffectiveName()), "-d")) + } + + [] + member _.CliFlag_Returns_Name_When_ShortForm_Null_And_Preferred() = async { + let attribute = CliFlagAttribute("--debug") + attribute.PreferShortForm <- true + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(attribute.GetEffectiveName()), "--debug")) + } + + [] + [] + [] + [] + member _.CliOption_GetSeparator_Returns_Correct_Separator(format: OptionFormat, expected: string) = async { + let attribute = CliOptionAttribute("--namespace") + attribute.Format <- format + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(attribute.GetSeparator()), expected)) + } + + [] + member _.CliOption_CustomSeparator_Overrides_Format() = async { + let attribute = CliOptionAttribute("--namespace") + attribute.Format <- OptionFormat.SpaceSeparated + attribute.CustomSeparator <- "::" + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(attribute.GetSeparator()), "::")) + } + + [] + member _.CliOption_Returns_Name_When_ShortForm_Not_Preferred() = async { + let attribute = CliOptionAttribute("--namespace") + attribute.ShortForm <- "-n" + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(attribute.GetEffectiveName()), "--namespace")) + } + + [] + member _.CliOption_Returns_ShortForm_When_Preferred() = async { + let attribute = CliOptionAttribute("--namespace") + attribute.ShortForm <- "-n" + attribute.PreferShortForm <- true + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(attribute.GetEffectiveName()), "-n")) + } + + [] + member _.CliArgument_Defaults_To_AfterOptions_Placement() = async { + let attribute = CliArgumentAttribute(0) + do! check(Assert.That(attribute.Placement = ArgumentPlacement.AfterOptions).IsTrue()) + } + + [] + member _.CliArgument_Position_Is_Set_Correctly() = async { + let attribute = CliArgumentAttribute(2) + do! check(Assert.That(attribute.Position = 2).IsTrue()) + } + + [] + member this.Parser_Handles_CliFlag() = async { + let options = { Debug = System.Nullable true } + let list = this.BuildArguments(options) + do! check(Assert.That(list |> Seq.toArray).IsEquivalentTo([| "--debug" |])) + } + + [] + member this.Parser_Omits_CliFlag_When_False() = async { + let options = { Debug = System.Nullable false } + let list = this.BuildArguments(options) + do! check(Assert.That((Seq.length list) = 0).IsTrue()) + } + + [] + member this.Parser_Omits_CliFlag_When_Null() = async { + let options = { Debug = System.Nullable() } + let list = this.BuildArguments(options) + do! check(Assert.That((Seq.length list) = 0).IsTrue()) + } + + [] + member this.Parser_Handles_CliOption_With_Space_Separator() = async { + let options = { Namespace = "default" } + let list = this.BuildArguments(options) + do! check(Assert.That(list |> Seq.toArray).IsEquivalentTo([| "--namespace"; "default" |])) + } + + [] + member this.Parser_Handles_CliOption_With_Equals_Separator() = async { + let options = { Set = "key=value" } + let list = this.BuildArguments(options) + do! check(Assert.That(list |> Seq.toArray).IsEquivalentTo([| "--set=key=value" |])) + } + + [] + member this.Parser_Handles_CliOption_With_Multiple_Values() = async { + let options = { Values = [| "file1.yaml"; "file2.yaml" |] } + let list = this.BuildArguments(options) + do! check(Assert.That(list |> Seq.toArray).IsEquivalentTo([| "--values"; "file1.yaml"; "--values"; "file2.yaml" |])) + } + + [] + member this.Parser_Handles_CliArgument_After_Options() = async { + let options = { ReleaseName = "myrelease"; Debug = System.Nullable true } + let list = this.BuildArguments(options) + do! check(Assert.That(list |> Seq.toArray).IsEquivalentTo([| "--debug"; "myrelease" |])) + } + + [] + member this.Parser_Handles_CliArgument_Before_Options() = async { + let options = { Path = "/some/path"; Debug = System.Nullable true } + let list = this.BuildArguments(options) + do! check(Assert.That(list |> Seq.toArray).IsEquivalentTo([| "/some/path"; "--debug" |])) + } + + [] + member this.Parser_Omits_Null_CliArgument() = async { + let options = { ReleaseName = null; Debug = System.Nullable true } + let list = this.BuildArguments(options) + do! check(Assert.That(list |> Seq.toArray).IsEquivalentTo([| "--debug" |])) + } + + [] + member this.Parser_Orders_Multiple_Arguments_By_Position() = async { + let options = { ReleaseName = "myrelease"; ChartReference = "bitnami/nginx" } + let list = this.BuildArguments(options) + do! check(Assert.That(list |> Seq.toArray).IsEquivalentTo([| "myrelease"; "bitnami/nginx" |])) + } + + [] + member this.Parser_Handles_Mixed_Flags_Options_And_Arguments() = async { + let options = + { + ReleaseName = "myrelease" + ChartReference = "bitnami/nginx" + Namespace = "production" + Debug = System.Nullable true + Set = [| "key1=val1"; "key2=val2" |] + } + + let list = this.BuildArguments(options) + + let firstItem = list |> Seq.tryHead + + do! check(Assert.That(firstItem.IsSome).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(firstItem.Value), "--debug")) + do! check(Assert.That(list |> Seq.contains "--namespace").IsTrue()) + do! check(Assert.That(list |> Seq.contains "production").IsTrue()) + do! check(Assert.That(list |> Seq.contains "--set=key1=val1").IsTrue()) + do! check(Assert.That(list |> Seq.contains "--set=key2=val2").IsTrue()) + do! check(Assert.That(list |> Seq.contains "myrelease").IsTrue()) + do! check(Assert.That(list |> Seq.contains "bitnami/nginx").IsTrue()) + } + + diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/CliToolAttributeTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/CliToolAttributeTests.fs new file mode 100644 index 0000000000..5223c645c8 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/CliToolAttributeTests.fs @@ -0,0 +1,69 @@ +namespace ModularPipelines.UnitTests.FSharp.Attributes + +open System.Reflection +open ModularPipelines.Attributes +open ModularPipelines.Options +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +[] +[] +type private TestGitOptions() = + inherit CommandLineToolOptions() + +[] +type private TestGitCommitOptions() = + inherit TestGitOptions() + +[] +[] +type private TestOptionsWithAttribute() = + inherit CommandLineToolOptions() + +type CliToolAttributeTests() = + [] + member _.CliToolAttribute_StoresToolName() = async { + let attribute = CliToolAttribute("git") + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(attribute.Tool), "git")) + } + + [] + member _.CliToolAttribute_CanBeAppliedToClass() = async { + let attribute = typeof.GetCustomAttribute() + do! check(Assert.That(attribute).IsNotNull()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(attribute.Tool), "git")) + } + + [] + member _.CliToolAttribute_ThrowsOnNullOrWhitespace() = async { + let mutable threw1 = false + try + CliToolAttribute(null) |> ignore + with :? System.ArgumentException -> + threw1 <- true + + let mutable threw2 = false + try + CliToolAttribute("") |> ignore + with :? System.ArgumentException -> + threw2 <- true + + let mutable threw3 = false + try + CliToolAttribute(" ") |> ignore + with :? System.ArgumentException -> + threw3 <- true + + do! check(Assert.That(threw1).IsTrue()) + do! check(Assert.That(threw2).IsTrue()) + do! check(Assert.That(threw3).IsTrue()) + } + + [] + member _.CliToolAttribute_IsInheritedByDerivedClasses() = async { + let attribute = typeof.GetCustomAttribute(true) + do! check(Assert.That(attribute).IsNotNull()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(attribute.Tool), "git")) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/DotNetFormatOptionsTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/DotNetFormatOptionsTests.fs new file mode 100644 index 0000000000..25fb806f5a --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/DotNetFormatOptionsTests.fs @@ -0,0 +1,28 @@ +namespace ModularPipelines.UnitTests.FSharp.Attributes + +open ModularPipelines.DotNet.Options +open ModularPipelines.Helpers.Internal +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type DotNetFormatOptionsTests() = + member private _.ModelProvider = CommandModelProvider() + member private _.ArgumentBuilder = CommandArgumentBuilder() + member private this.BuildArguments(optionsObject: obj) = + let model = this.ModelProvider.GetCommandModel(optionsObject.GetType()) + this.ArgumentBuilder.BuildArguments(model, optionsObject) + + [] + member this.ExcludeDiagnostics_Passes_Each_Id_Separately() = async { + let options = DotNetFormatOptions() + options.ExcludeDiagnostics <- [| "CS0246"; "CS1503" |] + + let args = this.BuildArguments(options) + + do! check(Assert.That((args |> Seq.toArray) = [| + "--exclude-diagnostics"; "CS0246" + "--exclude-diagnostics"; "CS1503" + |]).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/DynamicDependencyIntegrationTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/DynamicDependencyIntegrationTests.fs new file mode 100644 index 0000000000..a8ab5b549f --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/DynamicDependencyIntegrationTests.fs @@ -0,0 +1,65 @@ +namespace ModularPipelines.UnitTests.FSharp.Attributes + +open System +open System.Threading +open System.Threading.Tasks +open ModularPipelines.Attributes.Events +open ModularPipelines.Context +open ModularPipelines.Enums +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module DynamicDependencyIntegrationTests = + let executionOrder = ResizeArray() + + type AddDependencyAttribute(dependencyType: Type) = + inherit Attribute() + interface IModuleRegistrationEventReceiver with + member _.OnRegistrationAsync(context: IModuleRegistrationContext) = + context.AddDependency(dependencyType) + Task.CompletedTask + + type ModuleA() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + executionOrder.Add("A") + do! Task.Yield() + return "A" + } + + [)>] + type ModuleB() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + executionOrder.Add("B") + do! Task.Yield() + return "B" + } + +[] +type DynamicDependencyIntegrationTests() = + inherit TestBase() + + [] + member _.ClearExecutionOrder() = + DynamicDependencyIntegrationTests.executionOrder.Clear() + + [] + member _.DynamicDependency_ModuleBWaitsForModuleA() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status = Status.Successful).IsTrue()) + do! check(Assert.That((DynamicDependencyIntegrationTests.executionOrder |> Seq.toArray) = [| "A"; "B" |]).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/EnumValueAttributeTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/EnumValueAttributeTests.fs new file mode 100644 index 0000000000..773a653554 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/EnumValueAttributeTests.fs @@ -0,0 +1,40 @@ +namespace ModularPipelines.UnitTests.FSharp.Attributes + +open ModularPipelines.Attributes +open ModularPipelines.Helpers.Internal +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type Number = + | [] One = 0 + | [] Two = 1 + | [] Three = 2 + +[] +type private NumberWrapper = + { + [] + Number: Number + } + +type EnumValueAttributeTests() = + member private _.ModelProvider = CommandModelProvider() + member private _.ArgumentBuilder = CommandArgumentBuilder() + member private this.BuildArguments(optionsObject: obj) = + let model = this.ModelProvider.GetCommandModel(optionsObject.GetType()) + this.ArgumentBuilder.BuildArguments(model, optionsObject) + + [] + [] + [] + [] + member this.Can_Parse_EnumValueAttribute(number: Number, expected: string) = async { + let options = { Number = number } + + let list = this.BuildArguments(options) + do! check(Assert.That(list |> Seq.contains "--number").IsTrue()) + do! check(Assert.That(list |> Seq.contains expected).IsTrue()) + do! check(Assert.That((list |> Seq.toArray) = [| "--number"; expected |]).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/LifecycleEventIntegrationTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/LifecycleEventIntegrationTests.fs new file mode 100644 index 0000000000..c41dbd526f --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/LifecycleEventIntegrationTests.fs @@ -0,0 +1,132 @@ +namespace ModularPipelines.UnitTests.FSharp.Attributes + +open System +open System.Threading +open System.Threading.Tasks +open ModularPipelines.Attributes.Events +open ModularPipelines.Configuration +open ModularPipelines.Context +open ModularPipelines.Enums +open ModularPipelines.Extensions +open ModularPipelines.Models +open ModularPipelines.Modules +open ModularPipelines.Options +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module LifecycleEventIntegrationTests = + let eventLog = ResizeArray() + + type LogStartAttribute() = + inherit Attribute() + interface IModuleStartHandler with + member _.ContinueOnError = false + member _.OnModuleStartAsync(context: IModuleHookContext) = + eventLog.Add($"Start:{context.ModuleName}") + Task.CompletedTask + + type LogEndAttribute() = + inherit Attribute() + interface IModuleEndHandler with + member _.ContinueOnError = false + member _.OnModuleEndAsync(context: IModuleHookContext, _: IModuleResult) = + eventLog.Add($"End:{context.ModuleName}") + Task.CompletedTask + + type LogFailedAttribute() = + inherit Attribute() + interface IModuleFailureHandler with + member _.ContinueOnError = true + member _.OnModuleFailureAsync(context: IModuleHookContext, ex: Exception) = + eventLog.Add($"Failed:{context.ModuleName}:{ex.Message}") + Task.CompletedTask + + type LogSkippedAttribute() = + inherit Attribute() + interface IModuleSkippedHandler with + member _.ContinueOnError = false + member _.OnModuleSkippedAsync(context: IModuleHookContext, reason: SkipDecision) = + eventLog.Add($"Skipped:{context.ModuleName}:{reason.Reason}") + Task.CompletedTask + + [] + [] + type SuccessfulModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + return "Success" + } + + [] + [] + type FailingModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) : Task = + raise (InvalidOperationException("Intentional failure")) + + [] + [] + type SkippingModule() = + inherit Module() + override _.Configure() = + ModuleConfiguration.Create() + .WithSkipWhen(fun () -> SkipDecision.Skip("Test skip reason")) + .Build() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("Should not execute") + +[] +type LifecycleEventIntegrationTests() = + inherit TestBase() + + [] + member _.ClearEventLog() = + LifecycleEventIntegrationTests.eventLog.Clear() + + [] + member _.SuccessfulModule_InvokesStartAndEndEvents() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status = Status.Successful).IsTrue()) + do! check(Assert.That(LifecycleEventIntegrationTests.eventLog |> Seq.contains "Start:SuccessfulModule").IsTrue()) + do! check(Assert.That(LifecycleEventIntegrationTests.eventLog |> Seq.contains "End:SuccessfulModule").IsTrue()) + } + + [] + member _.FailingModule_InvokesStartAndFailedEvents() = async { + try + do! + TestPipelineHostBuilder.Create() + .AddModule() + .ConfigurePipelineOptions(fun _ options -> + options.ExecutionMode <- ExecutionMode.WaitForAllModules) + .ExecutePipelineAsync() + |> Async.AwaitTask + |> Async.Ignore + with _ -> () + + do! check(Assert.That(LifecycleEventIntegrationTests.eventLog |> Seq.contains "Start:FailingModule").IsTrue()) + do! check(Assert.That(LifecycleEventIntegrationTests.eventLog |> Seq.exists (fun e -> e.StartsWith("Failed:FailingModule:"))).IsTrue()) + } + + [] + member _.SkippingModule_InvokesStartAndSkippedEvents() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status = Status.Successful).IsTrue()) + do! check(Assert.That(LifecycleEventIntegrationTests.eventLog |> Seq.contains "Start:SkippingModule").IsTrue()) + do! check(Assert.That(LifecycleEventIntegrationTests.eventLog |> Seq.exists (fun e -> e.Contains("Skipped:SkippingModule:Test skip reason"))).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/LinuxOnlyTestAttribute.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/LinuxOnlyTestAttribute.fs new file mode 100644 index 0000000000..0283999d7d --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/LinuxOnlyTestAttribute.fs @@ -0,0 +1,10 @@ +namespace ModularPipelines.UnitTests.FSharp.Attributes + +open System +open System.Threading.Tasks +open TUnit.Core + +type LinuxOnlyTestAttribute() = + inherit SkipAttribute("Linux only test") + override _.ShouldSkip(_: TestRegisteredContext) = + Task.FromResult(not (OperatingSystem.IsLinux())) diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/MetadataCrossPhaseIntegrationTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/MetadataCrossPhaseIntegrationTests.fs new file mode 100644 index 0000000000..bcb033e6be --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/MetadataCrossPhaseIntegrationTests.fs @@ -0,0 +1,80 @@ +namespace ModularPipelines.UnitTests.FSharp.Attributes + +open System +open System.Threading +open System.Threading.Tasks +open ModularPipelines.Attributes.Events +open ModularPipelines.Context +open ModularPipelines.Enums +open ModularPipelines.Extensions +open ModularPipelines.Models +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module MetadataCrossPhaseIntegrationTests = + let eventLog = ResizeArray() + + type SetMetadataOnRegistrationAttribute(key: string, value: string) = + inherit Attribute() + interface IModuleRegistrationEventReceiver with + member _.OnRegistrationAsync(context: IModuleRegistrationContext) = + context.SetMetadata(key, value) + eventLog.Add($"Registration:SetMetadata:{key}={value}") + Task.CompletedTask + + type ReadMetadataOnStartAttribute(key: string) = + inherit Attribute() + interface IModuleStartHandler with + member _.ContinueOnError = false + member _.OnModuleStartAsync(context: IModuleHookContext) = + let value = context.GetMetadata(key) + let valueText = if value = null then "null" else value + eventLog.Add(sprintf "Start:ReadMetadata:%s=%s" key valueText) + Task.CompletedTask + + type ReadMetadataOnEndAttribute(key: string) = + inherit Attribute() + interface IModuleEndHandler with + member _.ContinueOnError = false + member _.OnModuleEndAsync(context: IModuleHookContext, _: IModuleResult) = + let value = context.GetMetadata(key) + let valueText = if value = null then "null" else value + eventLog.Add(sprintf "End:ReadMetadata:%s=%s" key valueText) + Task.CompletedTask + + [] + [] + [] + type MetadataModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + return "Done" + } + +[] +type MetadataCrossPhaseIntegrationTests() = + inherit TestBase() + + [] + member _.ClearEventLog() = + MetadataCrossPhaseIntegrationTests.eventLog.Clear() + + [] + member _.Metadata_SetDuringRegistration_AvailableDuringLifecycleEvents() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status = Status.Successful).IsTrue()) + do! check(Assert.That(MetadataCrossPhaseIntegrationTests.eventLog |> Seq.contains "Registration:SetMetadata:config=value-from-registration").IsTrue()) + do! check(Assert.That(MetadataCrossPhaseIntegrationTests.eventLog |> Seq.contains "Start:ReadMetadata:config=value-from-registration").IsTrue()) + do! check(Assert.That(MetadataCrossPhaseIntegrationTests.eventLog |> Seq.contains "End:ReadMetadata:config=value-from-registration").IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleAttributeEventServiceTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleAttributeEventServiceTests.fs new file mode 100644 index 0000000000..18aa128b60 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleAttributeEventServiceTests.fs @@ -0,0 +1,141 @@ +namespace ModularPipelines.UnitTests.FSharp.Attributes + +open System +open System.Threading +open System.Threading.Tasks +open ModularPipelines.Attributes.Events +open ModularPipelines.Context +open ModularPipelines.Engine.Attributes +open ModularPipelines.Modules +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module ModuleAttributeEventServiceTests = + type TestStartAttribute() = + inherit Attribute() + interface IModuleStartHandler with + member _.ContinueOnError = false + member _.OnModuleStartAsync(_: IModuleHookContext) = Task.CompletedTask + + type TestFailureAttribute() = + inherit Attribute() + interface IModuleFailureHandler with + member _.ContinueOnError = false + member _.OnModuleFailureAsync(_: IModuleHookContext, _: Exception) = Task.CompletedTask + + [] + type LowPriorityStartAttribute() = + inherit Attribute() + interface IModuleStartHandler with + member _.ContinueOnError = false + member _.OnModuleStartAsync(_: IModuleHookContext) = Task.CompletedTask + interface IEventHandlerPriority with + member _.Priority = 100 + + [] + type MediumPriorityStartAttribute() = + inherit Attribute() + interface IModuleStartHandler with + member _.ContinueOnError = false + member _.OnModuleStartAsync(_: IModuleHookContext) = Task.CompletedTask + interface IEventHandlerPriority with + member _.Priority = 10 + + [] + type HighPriorityStartAttribute() = + inherit Attribute() + interface IModuleStartHandler with + member _.ContinueOnError = false + member _.OnModuleStartAsync(_: IModuleHookContext) = Task.CompletedTask + interface IEventHandlerPriority with + member _.Priority = 1 + + [] + [] + type ModuleWithAttributes() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("test") + + type ModuleWithoutAttributes() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("test") + + [] + [] + [] + type ModuleWithPrioritizedHandlers() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("test") + + [] + [] + [] + type ModuleWithMixedPriorityHandlers() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("test") + +type ModuleAttributeEventServiceTests() = + [] + member _.GetStartHandlers_ModuleWithAttribute_ReturnsHandler() = async { + let service = ModuleAttributeEventService() + let handlers = service.GetStartHandlers(typeof) + do! check(Assert.That(handlers.Count = 1).IsTrue()) + do! check(Assert.That(handlers.[0]).IsTypeOf()) + } + + [] + member _.GetFailureHandlers_ModuleWithAttribute_ReturnsHandler() = async { + let service = ModuleAttributeEventService() + let handlers = service.GetFailureHandlers(typeof) + do! check(Assert.That(handlers.Count = 1).IsTrue()) + do! check(Assert.That(handlers.[0]).IsTypeOf()) + } + + [] + member _.GetStartHandlers_ModuleWithoutAttributes_ReturnsEmpty() = async { + let service = ModuleAttributeEventService() + let handlers = service.GetStartHandlers(typeof) + do! check(Assert.That(handlers.Count = 0).IsTrue()) + } + + [] + member _.GetHandlers_CachesResults() = async { + let service = ModuleAttributeEventService() + let handlers1 = service.GetStartHandlers(typeof) + let handlers2 = service.GetStartHandlers(typeof) + do! check(Assert.That(System.Object.ReferenceEquals(handlers1, handlers2)).IsTrue()) + } + + [] + member _.GetStartHandlers_WithPriority_ReturnsSortedByPriority() = async { + let service = ModuleAttributeEventService() + let handlers = service.GetStartHandlers(typeof) + do! check(Assert.That(handlers.Count = 3).IsTrue()) + do! check(Assert.That(handlers.[0]).IsTypeOf()) + do! check(Assert.That(handlers.[1]).IsTypeOf()) + do! check(Assert.That(handlers.[2]).IsTypeOf()) + } + + [] + member _.GetStartHandlers_WithMixedPriority_DefaultsToZero() = async { + let service = ModuleAttributeEventService() + let handlers = service.GetStartHandlers(typeof) + do! check(Assert.That(handlers.Count = 3).IsTrue()) + do! check(Assert.That(handlers.[0]).IsTypeOf()) + do! check(Assert.That(handlers.[1]).IsTypeOf()) + do! check(Assert.That(handlers.[2]).IsTypeOf()) + } + + [] + member _.GetStartHandlers_SingleHandler_ReturnsWithoutSorting() = async { + let service = ModuleAttributeEventService() + let handlers = service.GetStartHandlers(typeof) + do! check(Assert.That(handlers.Count = 1).IsTrue()) + do! check(Assert.That(handlers.[0]).IsTypeOf()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleDependencyRegistryTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleDependencyRegistryTests.fs new file mode 100644 index 0000000000..db8b0afb91 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleDependencyRegistryTests.fs @@ -0,0 +1,63 @@ +namespace ModularPipelines.UnitTests.FSharp.Attributes + +open System.Threading +open System.Threading.Tasks +open ModularPipelines.Context +open ModularPipelines.Engine.Dependencies +open ModularPipelines.Modules +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module ModuleDependencyRegistryTests = + type ModuleA() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("A") + + type ModuleB() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("B") + + type ModuleC() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("C") + +type ModuleDependencyRegistryTests() = + [] + member _.AddDynamicDependency_AddsDependency() = async { + let registry = ModuleDependencyRegistry() + registry.AddDynamicDependency(typeof, typeof) + let dependencies = registry.GetDynamicDependencies(typeof) + do! check(Assert.That(dependencies |> Seq.contains typeof).IsTrue()) + } + + [] + member _.AddDynamicDependency_MultipleDependencies_AddsAll() = async { + let registry = ModuleDependencyRegistry() + registry.AddDynamicDependency(typeof, typeof) + registry.AddDynamicDependency(typeof, typeof) + let dependencies = registry.GetDynamicDependencies(typeof) + do! check(Assert.That((dependencies |> Seq.length) = 2).IsTrue()) + do! check(Assert.That(dependencies |> Seq.contains typeof).IsTrue()) + do! check(Assert.That(dependencies |> Seq.contains typeof).IsTrue()) + } + + [] + member _.RemoveDependency_RemovesDependency() = async { + let registry = ModuleDependencyRegistry() + registry.AddDynamicDependency(typeof, typeof) + registry.RemoveDependency(typeof, typeof) + let dependencies = registry.GetDynamicDependencies(typeof) + do! check(Assert.That(Seq.isEmpty dependencies).IsTrue()) + } + + [] + member _.GetDynamicDependencies_NoDependencies_ReturnsEmpty() = async { + let registry = ModuleDependencyRegistry() + let dependencies = registry.GetDynamicDependencies(typeof) + do! check(Assert.That(Seq.isEmpty dependencies).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleMetadataRegistryTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleMetadataRegistryTests.fs new file mode 100644 index 0000000000..ec220814a3 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleMetadataRegistryTests.fs @@ -0,0 +1,47 @@ +namespace ModularPipelines.UnitTests.FSharp.Attributes + +open System.Threading +open System.Threading.Tasks +open Microsoft.Extensions.Options +open ModularPipelines.Context +open ModularPipelines.Engine.Dependencies +open ModularPipelines.Modules +open ModularPipelines.Options +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module ModuleMetadataRegistryTests = + type ModuleA() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("A") + + let createRegistry () = + ModuleMetadataRegistry(Options.Create(ModuleRegistrationOptions())) + +type ModuleMetadataRegistryTests() = + [] + member _.SetMetadata_GetMetadata_ReturnsValue() = async { + let registry = ModuleMetadataRegistryTests.createRegistry() + registry.SetMetadata(typeof, "key", "value") + let result = registry.GetMetadata(typeof, "key") + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result), "value")) + } + + [] + member _.GetMetadata_NotSet_ReturnsNull() = async { + let registry = ModuleMetadataRegistryTests.createRegistry() + let result = registry.GetMetadata(typeof, "key") + do! check(Assert.That(result).IsNull()) + } + + [] + member _.SetMetadata_OverwritesExisting() = async { + let registry = ModuleMetadataRegistryTests.createRegistry() + registry.SetMetadata(typeof, "key", "value1") + registry.SetMetadata(typeof, "key", "value2") + let result = registry.GetMetadata(typeof, "key") + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result), "value2")) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleReadyEventTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleReadyEventTests.fs new file mode 100644 index 0000000000..c0ec6714a2 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleReadyEventTests.fs @@ -0,0 +1,168 @@ +namespace ModularPipelines.UnitTests.FSharp.Attributes + +open System +open System.Threading +open System.Threading.Tasks +open ModularPipelines.Attributes +open ModularPipelines.Attributes.Events +open ModularPipelines.Context +open ModularPipelines.Enums +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module ModuleReadyEventTests = + let eventLog = ResizeArray() + + type LogReadyAttribute() = + inherit Attribute() + interface IModuleReadyHandler with + member _.ContinueOnError = false + member _.OnModuleReadyAsync(context: IModuleHookContext) = + eventLog.Add($"Ready:{context.ModuleName}") + Task.CompletedTask + + type LogReadyWithTimingAttribute() = + inherit Attribute() + interface IModuleReadyHandler with + member _.ContinueOnError = false + member _.OnModuleReadyAsync(context: IModuleHookContext) = + eventLog.Add($"Ready:{context.ModuleName}:ElapsedTime:{context.ElapsedTime.TotalMilliseconds >= 0.0}") + Task.CompletedTask + + type LogReadyAndStartAttribute() = + inherit Attribute() + interface IModuleReadyHandler with + member _.ContinueOnError = false + member _.OnModuleReadyAsync(context: IModuleHookContext) = + eventLog.Add($"Ready:{context.ModuleName}") + Task.CompletedTask + interface IModuleStartHandler with + member _.ContinueOnError = false + member _.OnModuleStartAsync(context: IModuleHookContext) = + eventLog.Add($"Start:{context.ModuleName}") + Task.CompletedTask + + type ThrowingReadyAttribute() = + inherit Attribute() + interface IModuleReadyHandler with + member _.ContinueOnError = true + member _.OnModuleReadyAsync(_: IModuleHookContext) = + raise (InvalidOperationException("Ready event failed")) + + [] + type SimpleModuleWithReadyEvent() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("Done") + + [] + type ModuleWithTimingCheck() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("Done") + + [] + type ModuleWithReadyAndStart() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("Done") + + type DependencyModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, ct: CancellationToken) = + task { + do! Task.Delay(10, ct) + return "Dependency Done" + } + + [] + [)>] + type DependentModuleWithReadyEvent() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("Dependent Done") + + [] + type ModuleWithThrowingReadyEvent() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("Done") + +[] +type ModuleReadyEventTests() = + inherit TestBase() + + [] + member _.ClearEventLog() = + ModuleReadyEventTests.eventLog.Clear() + + [] + member _.ReadyEvent_IsFired_WhenModuleIsReady() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status = Status.Successful).IsTrue()) + do! check(Assert.That(ModuleReadyEventTests.eventLog |> Seq.contains "Ready:SimpleModuleWithReadyEvent").IsTrue()) + } + + [] + member _.ReadyEvent_ProvidesValidContext() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status = Status.Successful).IsTrue()) + do! check(Assert.That(ModuleReadyEventTests.eventLog |> Seq.exists (fun e -> e.Contains("Ready:ModuleWithTimingCheck:ElapsedTime:True"))).IsTrue()) + } + + [] + member _.ReadyEvent_FiresBeforeStartEvent() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status = Status.Successful).IsTrue()) + + let readyIndex = ModuleReadyEventTests.eventLog.IndexOf("Ready:ModuleWithReadyAndStart") + let startIndex = ModuleReadyEventTests.eventLog.IndexOf("Start:ModuleWithReadyAndStart") + + do! check(Assert.That(readyIndex).IsGreaterThanOrEqualTo(0)) + do! check(Assert.That(startIndex).IsGreaterThanOrEqualTo(0)) + do! check(Assert.That(readyIndex).IsLessThan(startIndex)) + } + + [] + member _.ReadyEvent_IsFired_WhenDependenciesComplete() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status = Status.Successful).IsTrue()) + do! check(Assert.That(ModuleReadyEventTests.eventLog |> Seq.contains "Ready:DependentModuleWithReadyEvent").IsTrue()) + } + + [] + member _.ReadyEvent_WithContinueOnError_DoesNotFailPipeline() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleRegistrationContextTests.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleRegistrationContextTests.fs new file mode 100644 index 0000000000..635289b5d4 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/ModuleRegistrationContextTests.fs @@ -0,0 +1,89 @@ +namespace ModularPipelines.UnitTests.FSharp.Attributes + +open System +open System.Threading +open System.Threading.Tasks +open Microsoft.Extensions.Configuration +open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Hosting +open Microsoft.Extensions.Options +open ModularPipelines.Attributes.Events +open ModularPipelines.Context +open ModularPipelines.Engine.Dependencies +open ModularPipelines.Modules +open ModularPipelines.Options +open Moq +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module ModuleRegistrationContextTests = + type ModuleA() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("A") + + type ModuleB() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("B") + + let createContext + (moduleType: Type) + (dependencyRegistry: IModuleDependencyRegistry option) + (metadataRegistry: IModuleMetadataRegistry option) + (registeredModules: Type list option) = + let configuration = Mock.Of() + let environment = Mock.Of() + let services = ServiceCollection() + let depReg = defaultArg dependencyRegistry (ModuleDependencyRegistry() :> IModuleDependencyRegistry) + let metaReg = defaultArg metadataRegistry (ModuleMetadataRegistry(Options.Create(ModuleRegistrationOptions())) :> IModuleMetadataRegistry) + let regModules = defaultArg registeredModules [ moduleType ] |> List.toArray :> System.Collections.Generic.IReadOnlyList + ModuleRegistrationContext( + moduleType, + moduleType.GetCustomAttributes(true) |> Array.choose (function :? Attribute as a -> Some a | _ -> None) |> Array.toList :> System.Collections.Generic.IReadOnlyList, + configuration, + environment, + regModules, + services, + depReg, + metaReg) + +type ModuleRegistrationContextTests() = + [] + member _.ModuleType_ReturnsCorrectType() = async { + let context = ModuleRegistrationContextTests.createContext typeof None None None + do! check(Assert.That(context.ModuleType).IsEqualTo(typeof)) + } + + [] + member _.AddDependency_AddsToDependencyRegistry() = async { + let dependencyRegistry = ModuleDependencyRegistry() + let context = ModuleRegistrationContextTests.createContext typeof (Some (dependencyRegistry :> IModuleDependencyRegistry)) None None + context.AddDependency() + let dependencies = dependencyRegistry.GetDynamicDependencies(typeof) + do! check(Assert.That(dependencies |> Seq.contains typeof).IsTrue()) + } + + [] + member _.IsModuleRegistered_RegisteredModule_ReturnsTrue() = async { + let registeredModules = [ typeof; typeof ] + let context = ModuleRegistrationContextTests.createContext typeof None None (Some registeredModules) + do! check(Assert.That(context.IsModuleRegistered()).IsTrue()) + } + + [] + member _.IsModuleRegistered_UnregisteredModule_ReturnsFalse() = async { + let registeredModules = [ typeof ] + let context = ModuleRegistrationContextTests.createContext typeof None None (Some registeredModules) + do! check(Assert.That(context.IsModuleRegistered()).IsFalse()) + } + + [] + member _.SetMetadata_GetMetadata_RoundTrips() = async { + let metadataRegistry = ModuleMetadataRegistry(Options.Create(ModuleRegistrationOptions())) + let context = ModuleRegistrationContextTests.createContext typeof None (Some (metadataRegistry :> IModuleMetadataRegistry)) None + context.SetMetadata("key", "value") + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(context.GetMetadata("key")), "value")) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Attributes/WindowsOnlyTestAttribute.fs b/test/ModularPipelines.UnitTests.FSharp/Attributes/WindowsOnlyTestAttribute.fs new file mode 100644 index 0000000000..fd43053d27 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Attributes/WindowsOnlyTestAttribute.fs @@ -0,0 +1,10 @@ +namespace ModularPipelines.UnitTests.FSharp.Attributes + +open System +open System.Threading.Tasks +open TUnit.Core + +type WindowsOnlyTestAttribute() = + inherit SkipAttribute("Windows only test") + override _.ShouldSkip(_: TestRegisteredContext) = + Task.FromResult(not (OperatingSystem.IsWindows())) diff --git a/test/ModularPipelines.UnitTests.FSharp/Models/CommandLineTests.fs b/test/ModularPipelines.UnitTests.FSharp/Models/CommandLineTests.fs new file mode 100644 index 0000000000..900fc41002 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Models/CommandLineTests.fs @@ -0,0 +1,36 @@ +namespace ModularPipelines.UnitTests.FSharp.Models + +open System.Collections.Generic +open ModularPipelines.Models +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type CommandLineTests() = + [] + member _.CommandLine_StoresToolAndArguments() = async { + let commandLine = CommandLine("git", ["add"; "--all"]) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(commandLine.Tool), "git")) + do! check(Assert.That((commandLine.Arguments |> Seq.toArray) = [| "add"; "--all" |]).IsTrue()) + } + + [] + member _.CommandLine_ToString_FormatsCorrectly() = async { + let commandLine = CommandLine("git", ["commit"; "-m"; "test message"]) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(commandLine.ToString()), "git commit -m test message")) + } + + [] + member _.CommandLine_EmptyArguments_ToStringShowsToolOnly() = async { + let commandLine = CommandLine("git", []) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(commandLine.ToString()), "git")) + } + + [] + member _.CommandLine_ArgumentsAreImmutable() = async { + let args = List(["add"]) + let commandLine = CommandLine("git", args) + args.Add("--all") + do! check(Assert.That(commandLine.Arguments |> Seq.length = 1).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Models/JsonSerializationTests.fs b/test/ModularPipelines.UnitTests.FSharp/Models/JsonSerializationTests.fs new file mode 100644 index 0000000000..53c134ff5a --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Models/JsonSerializationTests.fs @@ -0,0 +1,56 @@ +namespace ModularPipelines.UnitTests.FSharp.Models + +open System.Collections.Generic +open System.Text.Json +open Microsoft.Extensions.DependencyInjection +open ModularPipelines.Engine +open ModularPipelines.Extensions +open ModularPipelines.Models +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type private JsonModule1() = + inherit Module>() + override _.ExecuteAsync(_, _) = + task { + do! System.Threading.Tasks.Task.Yield() + let d = Dictionary() + d["Foo"] <- "Bar" + d["Hello"] <- "world!" + return d :> IDictionary + } + +type JsonSerializationTests() = + inherit TestBase() + + [] + member _.Test1() = async { + let! host = + TestPipelineHostBuilder.Create() + .AddModule() + .BuildHostAsync() + |> Async.AwaitTask + + let! pipelineSummary = host.ExecutePipelineAsync() |> Async.AwaitTask + + let resultRegistry = host.RootServices.GetRequiredService() + let module1Result = resultRegistry.GetResult>(typeof) + + let pipelineJson = JsonSerializer.Serialize(pipelineSummary) + let deserializedSummary = JsonSerializer.Deserialize(pipelineJson) + + do! check(Assert.That(pipelineJson).IsNotNull()) + do! check(Assert.That(pipelineJson).IsNotEmpty()) + do! check(Assert.That(deserializedSummary).IsNotNull()) + do! check(Assert.That(deserializedSummary.Start = pipelineSummary.Start).IsTrue()) + do! check(Assert.That(deserializedSummary.End = pipelineSummary.End).IsTrue()) + do! check(Assert.That(deserializedSummary.TotalDuration = pipelineSummary.TotalDuration).IsTrue()) + do! check(Assert.That(deserializedSummary.Modules |> Seq.isEmpty).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(module1Result.ValueOrDefault["Foo"].ToString()), "Bar")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(module1Result.ValueOrDefault["Hello"].ToString()), "world!")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(module1Result.ModuleName), typeof.Name)) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Models/KeyValueTests.fs b/test/ModularPipelines.UnitTests.FSharp/Models/KeyValueTests.fs new file mode 100644 index 0000000000..fbe9468c06 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Models/KeyValueTests.fs @@ -0,0 +1,31 @@ +namespace ModularPipelines.UnitTests.FSharp.Models + +open System +open System.Collections.Generic +open ModularPipelines.Models +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type KeyValueTests() = + [] + member _.ImplicitOperator1() = async { + let keyValue: KeyValue = ("one", "two") + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(keyValue.Key), "one")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(keyValue.Value), "two")) + } + + [] + member _.ImplicitOperator2() = async { + let keyValue: KeyValue = Tuple("one", "two") + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(keyValue.Key), "one")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(keyValue.Value), "two")) + } + + [] + member _.ImplicitOperator3() = async { + let keyValue: KeyValue = KeyValuePair("one", "two") + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(keyValue.Key), "one")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(keyValue.Value), "two")) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Models/MyModel.fs b/test/ModularPipelines.UnitTests.FSharp/Models/MyModel.fs new file mode 100644 index 0000000000..41b4a4c40b --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Models/MyModel.fs @@ -0,0 +1,9 @@ +namespace ModularPipelines.UnitTests.FSharp.Models + +open ModularPipelines.Attributes + +type MyModel() = + [] + member _.MySecret = "This is a secret value!" + + member _.NotASecret = "This is NOT a secret value!" diff --git a/test/ModularPipelines.UnitTests.FSharp/Models/RequirementDecisionTests.fs b/test/ModularPipelines.UnitTests.FSharp/Models/RequirementDecisionTests.fs new file mode 100644 index 0000000000..bc82799536 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Models/RequirementDecisionTests.fs @@ -0,0 +1,57 @@ +namespace ModularPipelines.UnitTests.FSharp.Models + +open ModularPipelines.Models +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type RequirementDecisionTests() = + [] + member _.True_Implicit_Cast() = async { + let requirementDecision: RequirementDecision = true + do! check(Assert.That(requirementDecision.Success).IsTrue()) + do! check(Assert.That(requirementDecision.Reason).IsNull()) + } + + [] + member _.False_Implicit_Cast() = async { + let requirementDecision: RequirementDecision = false + do! check(Assert.That(requirementDecision.Success).IsFalse()) + do! check(Assert.That(requirementDecision.Reason).IsNull()) + } + + [] + member _.String_Implicit_Cast() = async { + let requirementDecision: RequirementDecision = "Foo!" + do! check(Assert.That(requirementDecision.Success).IsFalse()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(requirementDecision.Reason), "Foo!")) + } + + [] + member _.Failed() = async { + let requirementDecision = RequirementDecision.Failed("Blah!") + do! check(Assert.That(requirementDecision.Success).IsFalse()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(requirementDecision.Reason), "Blah!")) + } + + [] + member _.Passed() = async { + let requirementDecision = RequirementDecision.Passed + do! check(Assert.That(requirementDecision.Success).IsTrue()) + do! check(Assert.That(requirementDecision.Reason).IsNull()) + } + + [] + [] + [] + member _.Of(success: bool) = async { + let requirementDecision = RequirementDecision.Of(success, "Blah!") + + if success then + do! check(Assert.That(requirementDecision.Success).IsTrue()) + do! check(Assert.That(requirementDecision.Reason).IsNull()) + else + do! check(Assert.That(requirementDecision.Success).IsFalse()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(requirementDecision.Reason), "Blah!")) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Models/SkipDecisionTests.fs b/test/ModularPipelines.UnitTests.FSharp/Models/SkipDecisionTests.fs new file mode 100644 index 0000000000..9d79dcb699 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Models/SkipDecisionTests.fs @@ -0,0 +1,57 @@ +namespace ModularPipelines.UnitTests.FSharp.Models + +open ModularPipelines.Models +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type SkipDecisionTests() = + [] + member _.True_Implicit_Cast() = async { + let skipDecision: SkipDecision = true + do! check(Assert.That(skipDecision.ShouldSkip).IsTrue()) + do! check(Assert.That(skipDecision.Reason).IsNull()) + } + + [] + member _.String_Implicit_Cast() = async { + let skipDecision: SkipDecision = "Foo!" + do! check(Assert.That(skipDecision.ShouldSkip).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(skipDecision.Reason), "Foo!")) + } + + [] + member _.False_Implicit_Cast() = async { + let skipDecision: SkipDecision = false + do! check(Assert.That(skipDecision.ShouldSkip).IsFalse()) + do! check(Assert.That(skipDecision.Reason).IsNull()) + } + + [] + member _.Skip() = async { + let skipDecision = SkipDecision.Skip("Blah!") + do! check(Assert.That(skipDecision.ShouldSkip).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(skipDecision.Reason), "Blah!")) + } + + [] + member _.DoNotSkip() = async { + let skipDecision = SkipDecision.DoNotSkip + do! check(Assert.That(skipDecision.ShouldSkip).IsFalse()) + do! check(Assert.That(skipDecision.Reason).IsNull()) + } + + [] + [] + [] + member _.Of(shouldSkip: bool) = async { + let skipDecision = SkipDecision.Of(shouldSkip, "Blah!") + + if shouldSkip then + do! check(Assert.That(skipDecision.ShouldSkip).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(skipDecision.Reason), "Blah!")) + else + do! check(Assert.That(skipDecision.ShouldSkip).IsFalse()) + do! check(Assert.That(skipDecision.Reason).IsNull()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Models/TrxParsingTests.fs b/test/ModularPipelines.UnitTests.FSharp/Models/TrxParsingTests.fs new file mode 100644 index 0000000000..ef8f08c0ac --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Models/TrxParsingTests.fs @@ -0,0 +1,102 @@ +namespace ModularPipelines.UnitTests.FSharp.Models + +open System +open System.IO +open System.Linq +open Microsoft.Extensions.DependencyInjection +open ModularPipelines.Context +open ModularPipelines.DotNet +open ModularPipelines.DotNet.Enums +open ModularPipelines.DotNet.Extensions +open ModularPipelines.DotNet.Options +open ModularPipelines.DotNet.Parsers.Trx +open ModularPipelines.Engine +open ModularPipelines.Extensions +open ModularPipelines.Git.Extensions +open ModularPipelines.Models +open ModularPipelines.Modules +open ModularPipelines.Options +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type private NUnitModule() = + inherit Module() + + let mutable _lastResult: DotNetTestResult = Unchecked.defaultof<_> + + member _.LastResult = _lastResult + + override _.ExecuteAsync(context, cancellationToken) = + task { + let testProject = + context.Git().RootDirectory.FindFile(fun x -> x.Name = "ModularPipelines.TestsForTests.csproj") + + let resultsDirectory = Path.Combine(Path.GetTempPath(), System.Guid.NewGuid().ToString()) + Directory.CreateDirectory(resultsDirectory) |> ignore + + let trxFileName = "test-results.trx" + + let! _ = + context + .DotNet() + .Test( + DotNetTestOptions( + Framework = "net10.0", + ResultsDirectory = resultsDirectory, + Arguments = [| "--report-trx"; "--report-trx-filename"; trxFileName |] + ), + CommandExecutionOptions( + WorkingDirectory = testProject.Folder.Path, + ThrowOnNonZeroExitCode = false, + LogSettings = + CommandLoggingOptions( + Verbosity = CommandLogVerbosity.Minimal, + ShowStandardOutput = false, + ShowStandardError = true + ) + ), + cancellationToken + ) + + let trxFilePath = Path.Combine(resultsDirectory, trxFileName) + let! trxContents = File.ReadAllTextAsync(trxFilePath, cancellationToken) + _lastResult <- TrxParser().ParseTrxContents(trxContents) + return _lastResult + } + +type TrxParsingTests() = + inherit TestBase() + + [] + member _.NUnit() = async { + let! host = + TestPipelineHostBuilder.Create() + .AddModule() + .BuildHostAsync() + |> Async.AwaitTask + + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + + let modules = host.RootServices.GetServices() + let m = modules.OfType().First() + let testResult = m.LastResult + let failedCount = + testResult.UnitTestResults.Where(fun x -> x.Outcome = Nullable(TestOutcome.Failed)) + |> Seq.length + + let notExecutedCount = + testResult.UnitTestResults.Where(fun x -> x.Outcome = Nullable(TestOutcome.NotExecuted)) + |> Seq.length + + let passedCount = + testResult.UnitTestResults.Where(fun x -> x.Outcome = Nullable(TestOutcome.Passed)) + |> Seq.length + + do! check(Assert.That(testResult.Successful).IsFalse()) + do! check(Assert.That((failedCount = 1)).IsTrue()) + do! check(Assert.That((notExecutedCount = 1)).IsTrue()) + do! check(Assert.That((passedCount = 2)).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj b/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj index b00939dd2a..ec6badbd47 100644 --- a/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj +++ b/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj @@ -32,9 +32,15 @@ + + + + + + - + @@ -66,4 +72,4 @@ - \ No newline at end of file + diff --git a/test/ModularPipelines.UnitTests.FSharp/Modules/SyncModuleTests.fs b/test/ModularPipelines.UnitTests.FSharp/Modules/SyncModuleTests.fs new file mode 100644 index 0000000000..5ee4a304d4 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Modules/SyncModuleTests.fs @@ -0,0 +1,319 @@ +namespace ModularPipelines.UnitTests.FSharp.Modules + +open System.Collections.Generic +open System.Linq +open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Logging +open ModularPipelines.Configuration +open ModularPipelines.Context +open ModularPipelines.Engine +open ModularPipelines.Extensions +open ModularPipelines.Models +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open ModularPipelines.Enums + +type private SimpleSyncModule() = + inherit SyncModule() + override _.Execute(_, _) = "Hello from sync module" + +type private SyncModuleReturningNull() = + inherit SyncModule() + override _.Execute(_, _) = null + +type private SyncModuleWithComplexType() = + inherit SyncModule>() + override _.Execute(_, _) = + let d = Dictionary() + d["one"] <- 1 + d["two"] <- 2 + d["three"] <- 3 + d + +type private ThrowingSyncModule() = + inherit SyncModule() + override _.Execute(_, _) = + raise (System.InvalidOperationException("Sync module exception")) + +type private SyncModuleWithBeforeHook() = + inherit SyncModule() + let mutable _beforeCalled = false + member _.BeforeHookCalled = _beforeCalled + override _.OnBeforeExecute(_, _) = _beforeCalled <- true + override _.Execute(_, _) = "executed" + +type private SyncModuleWithAfterHook() = + inherit SyncModule() + let mutable _afterCalled = false + let mutable _capturedResult: ModuleResult = Unchecked.defaultof<_> + member _.AfterHookCalled = _afterCalled + member _.CapturedResult = _capturedResult + override _.OnAfterExecute(_, result, _) = + _afterCalled <- true + _capturedResult <- result + Unchecked.defaultof> + override _.Execute(_, _) = "original" + +type private SyncModuleWithFailedHook() = + inherit SyncModule() + let mutable _failedCalled = false + let mutable _capturedException: System.Exception = null + member _.FailedHookCalled = _failedCalled + member _.CapturedExceptionInHook = _capturedException + override _.OnFailed(_, ex, _) = + _failedCalled <- true + _capturedException <- ex + override _.Execute(_, _) = + raise (System.InvalidOperationException("Test failure")) + +type private SyncModuleWithSkipConfig() = + inherit SyncModule() + let mutable _skippedCalled = false + let mutable _capturedSkip: SkipDecision = Unchecked.defaultof<_> + member _.SkippedHookCalled = _skippedCalled + member _.CapturedSkipDecision = _capturedSkip + override _.Configure() = + ModuleConfiguration.Create().WithSkipWhen(fun () -> SkipDecision.Skip("Always skip")).Build() + override _.OnSkipped(_, skipDecision, _) = + _skippedCalled <- true + _capturedSkip <- skipDecision + override _.Execute(_, _) = "should not execute" + +type private SyncDependencyModule() = + inherit SyncModule() + override _.Execute(_, _) = 42 + +[)>] +type private SyncDependentModule() = + inherit SyncModule() + override _.Execute(context, _) = + let dep = context.GetModule().GetAwaiter().GetResult() + sprintf "Dependency value: %d" dep.ValueOrDefault + +type private AsyncDependencyModule() = + inherit Module() + override _.ExecuteAsync(_, _) = + task { + do! System.Threading.Tasks.Task.Yield() + return 100 + } + +[)>] +type private SyncDependsOnAsync() = + inherit SyncModule() + override _.Execute(context, _) = + let dep = context.GetModule().GetAwaiter().GetResult() + sprintf "Async dependency value: %d" dep.ValueOrDefault + +type private SyncModuleForAsyncToDepend() = + inherit SyncModule() + override _.Execute(_, _) = 200 + +[)>] +type private AsyncDependsOnSync() = + inherit Module() + override _.ExecuteAsync(context, _) = + task { + do! System.Threading.Tasks.Task.Yield() + let! dep = context.GetModule() + return sprintf "Sync dependency value: %d" dep.ValueOrDefault + } + +type private SyncModuleWithTimeout() = + inherit SyncModule() + override _.Configure() = + ModuleConfiguration.Create().WithTimeout(System.TimeSpan.FromMinutes(5.0)).Build() + override _.Execute(_, _) = "configured" + +type private SyncModuleWithRetry() = + inherit SyncModule() + let mutable _count = 0 + member _.ExecutionCount = _count + override _.Configure() = + ModuleConfiguration.Create().WithRetryCount(3).Build() + override _.Execute(_, _) = + _count <- _count + 1 + if _count < 3 then + raise (System.InvalidOperationException(sprintf "Attempt %d failed" _count)) + "success on third try" + +type private SyncModuleCheckingCancellation() = + inherit SyncModule() + override _.Execute(_, cancellationToken) = + cancellationToken.ThrowIfCancellationRequested() + "not cancelled" + +type private SyncModuleAccessingContext() = + inherit SyncModule() + override _.Execute(context, _) = + context.Logger.LogInformation("Logging from sync module") + "context accessed" + +type SyncModuleTests() = + inherit TestBase() + + [] + member this.SyncModule_Executes_And_Returns_Value() = async { + let! m = this.RunModule() |> Async.AwaitTask + let! result = m.CompletionSource.Task |> Async.AwaitTask + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.ValueOrDefault), "Hello from sync module")) + do! check(Assert.That(result.ModuleResultType = ModuleResultType.Success).IsTrue()) + } + + [] + member this.SyncModule_Can_Return_Null() = async { + let! m = this.RunModule() |> Async.AwaitTask + let! result = m.CompletionSource.Task |> Async.AwaitTask + do! check(Assert.That(result.ValueOrDefault).IsNull()) + do! check(Assert.That(result.ModuleResultType = ModuleResultType.Success).IsTrue()) + } + + [] + member this.SyncModule_Can_Return_Complex_Types() = async { + let! m = this.RunModule() |> Async.AwaitTask + let! result = m.CompletionSource.Task |> Async.AwaitTask + do! check(Assert.That(result.ValueOrDefault <> null).IsTrue()) + do! check(Assert.That(result.ValueOrDefault.Count = 3).IsTrue()) + do! check(Assert.That(result.ValueOrDefault["two"] = 2).IsTrue()) + } + + [] + member _.SyncModule_Exception_Is_Captured() = async { + let! host = + TestPipelineHostBuilder.Create().AddModule().BuildHostAsync() + |> Async.AwaitTask + + try + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + with _ -> () + + let resultRegistry = host.RootServices.GetRequiredService() + let result = resultRegistry.GetResult(typeof) + + do! check(Assert.That(result.ModuleStatus = Status.Failed).IsTrue()) + do! check(Assert.That(result.ExceptionOrDefault).IsNotNull()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.ExceptionOrDefault.Message), "Sync module exception")) + } + + [] + member this.SyncModule_OnBeforeExecute_Is_Called() = async { + let! m = this.RunModule() |> Async.AwaitTask + do! check(Assert.That(m.BeforeHookCalled).IsTrue()) + } + + [] + member this.SyncModule_OnAfterExecute_Is_Called() = async { + let! m = this.RunModule() |> Async.AwaitTask + do! check(Assert.That(m.AfterHookCalled).IsTrue()) + do! check(Assert.That(m.CapturedResult).IsNotNull()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(m.CapturedResult.ValueOrDefault), "original")) + } + + [] + member _.SyncModule_OnFailed_Is_Called_On_Exception() = async { + let! host = + TestPipelineHostBuilder.Create().AddModule().BuildHostAsync() + |> Async.AwaitTask + + try + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + with _ -> () + + let modules = host.RootServices.GetServices() + let m = modules.OfType().Single() + + do! check(Assert.That(m.FailedHookCalled).IsTrue()) + do! check(Assert.That(m.CapturedExceptionInHook).IsNotNull()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(m.CapturedExceptionInHook.Message), "Test failure")) + } + + [] + member _.SyncModule_OnSkipped_Is_Called_When_Skipped() = async { + let! host = + TestPipelineHostBuilder.Create().AddModule().BuildHostAsync() + |> Async.AwaitTask + + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + + let modules = host.RootServices.GetServices() + let m = modules.OfType().Single() + + do! check(Assert.That(m.SkippedHookCalled).IsTrue()) + do! check(Assert.That(m.CapturedSkipDecision).IsNotNull()) + do! check(Assert.That(m.CapturedSkipDecision.ShouldSkip).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(m.CapturedSkipDecision.Reason), "Always skip")) + } + + [] + member this.SyncModule_Can_Depend_On_Another_SyncModule() = async { + let! struct (dep, dependent) = this.RunModules() |> Async.AwaitTask + let! depResult = dep.CompletionSource.Task |> Async.AwaitTask + let! dependentResult = dependent.CompletionSource.Task |> Async.AwaitTask + do! check(Assert.That(depResult.ValueOrDefault = 42).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(dependentResult.ValueOrDefault), "Dependency value: 42")) + } + + [] + member this.SyncModule_Can_Depend_On_AsyncModule() = async { + let! struct (asyncModule, syncModule) = this.RunModules() |> Async.AwaitTask + let! asyncResult = asyncModule.CompletionSource.Task |> Async.AwaitTask + let! syncResult = syncModule.CompletionSource.Task |> Async.AwaitTask + do! check(Assert.That(asyncResult.ValueOrDefault = 100).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(syncResult.ValueOrDefault), "Async dependency value: 100")) + } + + [] + member this.AsyncModule_Can_Depend_On_SyncModule() = async { + let! struct (syncModule, asyncModule) = this.RunModules() |> Async.AwaitTask + let! syncResult = syncModule.CompletionSource.Task |> Async.AwaitTask + let! asyncResult = asyncModule.CompletionSource.Task |> Async.AwaitTask + do! check(Assert.That(syncResult.ValueOrDefault = 200).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(asyncResult.ValueOrDefault), "Sync dependency value: 200")) + } + + [] + member this.SyncModule_Respects_Configuration() = async { + let! m = this.RunModule() |> Async.AwaitTask + let imodule = m :> IModule + do! check(Assert.That(imodule.Configuration.Timeout = System.TimeSpan.FromMinutes(5.0)).IsTrue()) + } + + [] + member this.SyncModule_Respects_Retry_Configuration() = async { + let! m = this.RunModule() |> Async.AwaitTask + let! result = m.CompletionSource.Task |> Async.AwaitTask + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.ValueOrDefault), "success on third try")) + do! check(Assert.That(m.ExecutionCount = 3).IsTrue()) + } + + [] + member this.SyncModule_Receives_CancellationToken() = async { + let! m = this.RunModule() |> Async.AwaitTask + let! result = m.CompletionSource.Task |> Async.AwaitTask + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.ValueOrDefault), "not cancelled")) + } + + [] + member this.SyncModule_Can_Access_Context() = async { + let! m = this.RunModule() |> Async.AwaitTask + let! result = m.CompletionSource.Task |> Async.AwaitTask + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.ValueOrDefault), "context accessed")) + } + + [] + member this.SimpleSyncTestModule_Helper_Works() = async { + let! m = this.RunModule() |> Async.AwaitTask + let! result = m.CompletionSource.Task |> Async.AwaitTask + do! check(Assert.That(result.ValueOrDefault).IsTrue()) + } + + [] + member this.SyncNullModule_Helper_Works() = async { + let! m = this.RunModule() |> Async.AwaitTask + let! result = m.CompletionSource.Task |> Async.AwaitTask + do! check(Assert.That(result.ValueOrDefault).IsNull()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Modules/TestModule1.fs b/test/ModularPipelines.UnitTests.FSharp/Modules/TestModule1.fs new file mode 100644 index 0000000000..b178d5b742 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Modules/TestModule1.fs @@ -0,0 +1,8 @@ +namespace ModularPipelines.UnitTests.FSharp.Modules + +open System.Collections.Generic +open ModularPipelines.TestHelpers + +type TestModule1() = + inherit SimpleTestModule option>() + override _.Result = None diff --git a/test/ModularPipelines.UnitTests.FSharp/Requirements/PipelineRequirementBaseClassTests.fs b/test/ModularPipelines.UnitTests.FSharp/Requirements/PipelineRequirementBaseClassTests.fs new file mode 100644 index 0000000000..3e7dd2855b --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Requirements/PipelineRequirementBaseClassTests.fs @@ -0,0 +1,202 @@ +namespace ModularPipelines.UnitTests.FSharp.Requirements + +open System +open System.Threading +open Microsoft.Extensions.DependencyInjection +open ModularPipelines.Context +open ModularPipelines.Engine +open ModularPipelines.Exceptions +open ModularPipelines.Extensions +open ModularPipelines.Models +open ModularPipelines.Modules +open ModularPipelines.Requirements +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open ModularPipelines.Enums + +type private RequirementDummyModule() = + inherit Module() + override _.ExecuteAsync(_, _) = + task { + do! System.Threading.Tasks.Task.Yield() + return true + } + +type private PassingSyncRequirement() = + inherit PipelineRequirement() + override _.Must(_) = RequirementDecision.Passed + +type private FailingSyncRequirement() = + inherit PipelineRequirement() + override _.Must(_) = RequirementDecision.Failed("Sync requirement failed") + +type private PassingAsyncRequirement() = + inherit PipelineRequirement() + override _.MustAsync(_) = + task { + do! System.Threading.Tasks.Task.Yield() + return RequirementDecision.Passed + } + +type private FailingAsyncRequirement() = + inherit PipelineRequirement() + override _.MustAsync(_) = + task { + do! System.Threading.Tasks.Task.Yield() + return RequirementDecision.Failed("Async requirement failed") + } + +type private WhenTrueRequirement() = + inherit PipelineRequirement() + override _.Must(_) = RequirementDecision.Of(true, "Should not see this") + +type private WhenFalseRequirement() = + inherit PipelineRequirement() + override _.Must(_) = RequirementDecision.Of(false, "When condition failed") + +type private CustomOrderRequirement() = + inherit PipelineRequirement() + override _.Order = 10 + +[] +module private RequirementTestHelpers = + let unwrapMessage (ex: exn) = + match ex with + | :? AggregateException as aggregate -> aggregate.GetBaseException().Message + | _ -> ex.Message + + let isRequirementFailure (ex: exn) = + match ex with + | :? FailedRequirementsException -> true + | :? AggregateException as aggregate -> aggregate.GetBaseException() :? FailedRequirementsException + | _ -> false + +type PipelineRequirementBaseClassTests() = + [] + member _.Sync_Requirement_With_Pass_Succeeds() = async { + let! host = + TestPipelineHostBuilder + .Create() + .AddModule() + .AddRequirement() + .BuildHostAsync() + |> Async.AwaitTask + + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + + let resultRegistry = host.RootServices.GetRequiredService() + let result = resultRegistry.GetResult(typeof) + + do! check(Assert.That(result.ModuleStatus = Status.Successful).IsTrue()) + } + + [] + member _.Sync_Requirement_With_Fail_Throws() = async { + let mutable threw = false + let mutable exMessage = "" + + try + do! + TestPipelineHostBuilder + .Create() + .AddModule() + .AddRequirement() + .ExecutePipelineAsync() + |> Async.AwaitTask + |> Async.Ignore + with ex when isRequirementFailure ex -> + threw <- true + exMessage <- unwrapMessage ex + + do! check(Assert.That(threw).IsTrue()) + do! check(Assert.That(exMessage.Contains("Sync requirement failed")).IsTrue()) + } + + [] + member _.Async_Requirement_With_Pass_Succeeds() = async { + let! host = + TestPipelineHostBuilder + .Create() + .AddModule() + .AddRequirement() + .BuildHostAsync() + |> Async.AwaitTask + + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + + let resultRegistry = host.RootServices.GetRequiredService() + let result = resultRegistry.GetResult(typeof) + + do! check(Assert.That(result.ModuleStatus = Status.Successful).IsTrue()) + } + + [] + member _.Async_Requirement_With_Fail_Throws() = async { + let mutable threw = false + let mutable exMessage = "" + + try + do! + TestPipelineHostBuilder + .Create() + .AddModule() + .AddRequirement() + .ExecutePipelineAsync() + |> Async.AwaitTask + |> Async.Ignore + with ex when isRequirementFailure ex -> + threw <- true + exMessage <- unwrapMessage ex + + do! check(Assert.That(threw).IsTrue()) + do! check(Assert.That(exMessage.Contains("Async requirement failed")).IsTrue()) + } + + [] + member _.When_Helper_With_True_Passes() = async { + let! host = + TestPipelineHostBuilder + .Create() + .AddModule() + .AddRequirement() + .BuildHostAsync() + |> Async.AwaitTask + + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + + let resultRegistry = host.RootServices.GetRequiredService() + let result = resultRegistry.GetResult(typeof) + + do! check(Assert.That(result.ModuleStatus = Status.Successful).IsTrue()) + } + + [] + member _.When_Helper_With_False_Fails() = async { + let mutable threw = false + let mutable exMessage = "" + + try + do! + TestPipelineHostBuilder + .Create() + .AddModule() + .AddRequirement() + .ExecutePipelineAsync() + |> Async.AwaitTask + |> Async.Ignore + with ex when isRequirementFailure ex -> + threw <- true + exMessage <- unwrapMessage ex + + do! check(Assert.That(threw).IsTrue()) + do! check(Assert.That(exMessage.Contains("When condition failed")).IsTrue()) + } + + [] + member _.Custom_Order_Is_Respected() = async { + let requirement = CustomOrderRequirement() + do! check(Assert.That(requirement.Order = 10).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Requirements/RequireFactoryTests.fs b/test/ModularPipelines.UnitTests.FSharp/Requirements/RequireFactoryTests.fs new file mode 100644 index 0000000000..4e060ec2eb --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Requirements/RequireFactoryTests.fs @@ -0,0 +1,220 @@ +namespace ModularPipelines.UnitTests.FSharp.Requirements + +open System +open Microsoft.Extensions.DependencyInjection +open ModularPipelines.Context +open ModularPipelines.Engine +open ModularPipelines.Exceptions +open ModularPipelines.Extensions +open ModularPipelines.Models +open ModularPipelines.Modules +open ModularPipelines.Requirements +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open ModularPipelines.Enums + +type private RequireFactoryDummyModule() = + inherit Module() + override _.ExecuteAsync(_, _) = + task { + do! System.Threading.Tasks.Task.Yield() + return true + } + +[] +module private RequirementFactoryTestHelpers = + let unwrapMessage (ex: exn) = + match ex with + | :? AggregateException as aggregate -> aggregate.GetBaseException().Message + | _ -> ex.Message + + let isRequirementFailure (ex: exn) = + match ex with + | :? FailedRequirementsException -> true + | :? AggregateException as aggregate -> aggregate.GetBaseException() :? FailedRequirementsException + | _ -> false + +type RequireFactoryTests() = + [] + member _.Require_That_With_True_Condition_Passes() = async { + let! host = + TestPipelineHostBuilder + .Create() + .AddModule() + .AddRequirement(Require.That((fun _ -> true), "Should not fail")) + .BuildHostAsync() + |> Async.AwaitTask + + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + + let resultRegistry = host.RootServices.GetRequiredService() + let result = resultRegistry.GetResult(typeof) + + do! check(Assert.That(result.ModuleStatus = Status.Successful).IsTrue()) + } + + [] + member _.Require_That_With_False_Condition_Fails() = async { + let reason = "Custom failure reason" + let mutable threw = false + let mutable exMessage = "" + + try + do! + TestPipelineHostBuilder + .Create() + .AddModule() + .AddRequirement(Require.That((fun _ -> false), reason)) + .ExecutePipelineAsync() + |> Async.AwaitTask + |> Async.Ignore + with ex when isRequirementFailure ex -> + threw <- true + exMessage <- unwrapMessage ex + + do! check(Assert.That(threw).IsTrue()) + do! check(Assert.That(exMessage.Contains(reason)).IsTrue()) + } + + [] + member _.Require_ThatAsync_With_True_Condition_Passes() = async { + let! host = + TestPipelineHostBuilder + .Create() + .AddModule() + .AddRequirement( + Require.ThatAsync( + (fun _ -> + task { + do! System.Threading.Tasks.Task.Yield() + return true + }), + "Should not fail" + ) + ) + .BuildHostAsync() + |> Async.AwaitTask + + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + + let resultRegistry = host.RootServices.GetRequiredService() + let result = resultRegistry.GetResult(typeof) + + do! check(Assert.That(result.ModuleStatus = Status.Successful).IsTrue()) + } + + [] + member _.Require_ThatAsync_With_False_Condition_Fails() = async { + let reason = "Async failure reason" + let mutable threw = false + let mutable exMessage = "" + + try + do! + TestPipelineHostBuilder + .Create() + .AddModule() + .AddRequirement( + Require.ThatAsync( + (fun _ -> + task { + do! System.Threading.Tasks.Task.Yield() + return false + }), + reason + ) + ) + .ExecutePipelineAsync() + |> Async.AwaitTask + |> Async.Ignore + with ex when isRequirementFailure ex -> + threw <- true + exMessage <- unwrapMessage ex + + do! check(Assert.That(threw).IsTrue()) + do! check(Assert.That(exMessage.Contains(reason)).IsTrue()) + } + + [] + member _.Require_EnvironmentVariable_When_Set_Passes() = async { + let varName = "TEST_REQUIREMENT_VAR_FS" + Environment.SetEnvironmentVariable(varName, "some-value") + + try + let! host = + TestPipelineHostBuilder + .Create() + .AddModule() + .AddRequirement(Require.EnvironmentVariable(varName)) + .BuildHostAsync() + |> Async.AwaitTask + + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + + let resultRegistry = host.RootServices.GetRequiredService() + let result = resultRegistry.GetResult(typeof) + + do! check(Assert.That(result.ModuleStatus = Status.Successful).IsTrue()) + finally + Environment.SetEnvironmentVariable(varName, null) + } + + [] + member _.Require_EnvironmentVariable_When_Not_Set_Fails() = async { + let varName = "UNLIKELY_TO_EXIST_VAR_FS_12345" + Environment.SetEnvironmentVariable(varName, null) + + let mutable threw = false + let mutable exMessage = "" + + try + do! + TestPipelineHostBuilder + .Create() + .AddModule() + .AddRequirement(Require.EnvironmentVariable(varName)) + .ExecutePipelineAsync() + |> Async.AwaitTask + |> Async.Ignore + with ex when isRequirementFailure ex -> + threw <- true + exMessage <- unwrapMessage ex + + do! check(Assert.That(threw).IsTrue()) + do! check(Assert.That(exMessage.Contains(varName)).IsTrue()) + } + + [] + member _.Require_EnvironmentVariable_With_Custom_Reason() = async { + let varName = "UNLIKELY_TO_EXIST_VAR_FS_67890" + let customReason = "My custom message about the var" + Environment.SetEnvironmentVariable(varName, null) + + let mutable threw = false + let mutable exMessage = "" + + try + do! + TestPipelineHostBuilder + .Create() + .AddModule() + .AddRequirement(Require.EnvironmentVariable(varName, customReason)) + .ExecutePipelineAsync() + |> Async.AwaitTask + |> Async.Ignore + with ex when isRequirementFailure ex -> + threw <- true + exMessage <- unwrapMessage ex + + do! check(Assert.That(threw).IsTrue()) + do! check(Assert.That(exMessage.Contains(customReason)).IsTrue()) + } + + [] + member _.DelegateRequirement_Respects_Order() = async { + let requirement = Require.That((fun _ -> true), "test", order = 5) + do! check(Assert.That(requirement.Order = 5).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Results/ResultsRepositoryTests.fs b/test/ModularPipelines.UnitTests.FSharp/Results/ResultsRepositoryTests.fs new file mode 100644 index 0000000000..2798e65f94 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Results/ResultsRepositoryTests.fs @@ -0,0 +1,107 @@ +namespace ModularPipelines.UnitTests.FSharp.Results + +open System.Text.Json +open Microsoft.Extensions.DependencyInjection +open ModularPipelines.Context +open ModularPipelines.Engine +open ModularPipelines.Extensions +open ModularPipelines.FileSystem +open ModularPipelines.Models +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open ModularPipelines.Enums + +[] +module private ResultsRepositoryTestsHelpers = + let sharedFolder = Folder.CreateTemporaryFolder() + +type private JsonResultRepository() = + interface IModuleResultRepository with + member _.IsEnabled = true + + member _.SaveResultAsync(m, moduleResult, _) = + task { + let file = sharedFolder.CreateFile(m.GetType().FullName) + use! fileStream = System.Threading.Tasks.Task.FromResult(file.GetStream()) + do! JsonSerializer.SerializeAsync(fileStream, moduleResult) + } + + member _.GetResultAsync(m, _) = + task { + let file = sharedFolder.GetFile(m.GetType().FullName) + use! fileStream = System.Threading.Tasks.Task.FromResult(file.GetStream()) + return! JsonSerializer.DeserializeAsync>(fileStream) + } + +type private RepoModule1() = + inherit SimpleTestModule() + override _.Result = true + +[)>] +type private RepoModule2() = + inherit SimpleTestModule() + override _.Result = true + +[] +type ResultsRepositoryTests() = + inherit TestBase() + + [] + [] + member _.RunOne() = async { + let! host = + TestPipelineHostBuilder + .Create() + .AddResultsRepository() + .AddModule() + .AddModule() + .BuildHostAsync() + |> Async.AwaitTask + + do! (host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore) + + let resultRegistry = host.RootServices.GetRequiredService() + let module1Result = resultRegistry.GetResult(typeof) + let module2Result = resultRegistry.GetResult(typeof) + + do! check(Assert.That(module1Result.ModuleStatus).IsEqualTo(Status.Successful)) + do! check(Assert.That(module2Result.ModuleStatus).IsEqualTo(Status.Successful)) + } + + [] + [] + member _.RunTwoFromHistory() = async { + let! seedHost = + TestPipelineHostBuilder + .Create() + .AddResultsRepository() + .AddModule() + .AddModule() + .BuildHostAsync() + |> Async.AwaitTask + + do! (seedHost.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore) + + let! host = + TestPipelineHostBuilder + .Create() + .AddResultsRepository() + .AddModule() + .AddModule() + .RunCategories("Other") + .BuildHostAsync() + |> Async.AwaitTask + + do! (host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore) + + let resultRegistry = host.RootServices.GetRequiredService() + let module1Result = resultRegistry.GetResult(typeof) + let module2Result = resultRegistry.GetResult(typeof) + + do! check(Assert.That(module1Result.ModuleStatus).IsEqualTo(Status.UsedHistory)) + do! check(Assert.That(module2Result.ModuleStatus).IsEqualTo(Status.UsedHistory)) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Results/ReturnNothingTests.fs b/test/ModularPipelines.UnitTests.FSharp/Results/ReturnNothingTests.fs new file mode 100644 index 0000000000..7bc30b7b9c --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Results/ReturnNothingTests.fs @@ -0,0 +1,51 @@ +namespace ModularPipelines.UnitTests.FSharp.Results + +open ModularPipelines.Models +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type private ReturnNothingModule1() = + inherit SimpleTestModule() + override _.Result = Unchecked.defaultof + +type private ReturnNothingModule2() = + inherit SimpleTestModule() + override _.Result = Unchecked.defaultof + +type private ReturnNothingModule3() = + inherit SimpleTestModule() + override _.Result = Unchecked.defaultof + +type ReturnNothingTests() = + inherit TestBase() + + member private _.AssertResult(result: ModuleResult) = async { + do! check(Assert.That(result.IsSuccess).IsTrue()) + do! check(Assert.That(result.ModuleResultType = ModuleResultType.Success).IsTrue()) + do! check(Assert.That(result.ValueOrDefault).IsNull()) + do! check(Assert.That(result.ExceptionOrDefault).IsNull()) + } + + [] + member this.Module1_HasValue_False() = async { + let! m = this.RunModule() |> Async.AwaitTask + let! result = m.CompletionSource.Task |> Async.AwaitTask + do! this.AssertResult(result) + } + + [] + member this.Module2_HasValue_False() = async { + let! m = this.RunModule() |> Async.AwaitTask + let! result = m.CompletionSource.Task |> Async.AwaitTask + do! this.AssertResult(result) + } + + [] + member this.Module3_HasValue_False() = async { + let! m = this.RunModule() |> Async.AwaitTask + let! result = m.CompletionSource.Task |> Async.AwaitTask + do! this.AssertResult(result) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/State/ModuleExecutionPhaseTests.fs b/test/ModularPipelines.UnitTests.FSharp/State/ModuleExecutionPhaseTests.fs new file mode 100644 index 0000000000..66b0a87c8d --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/State/ModuleExecutionPhaseTests.fs @@ -0,0 +1,169 @@ +namespace ModularPipelines.UnitTests.FSharp.State + +open System +open System.Collections.Immutable +open ModularPipelines.Engine.State +open ModularPipelines.Enums +open ModularPipelines.Models +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type private TestModuleResult() = + interface IModuleResult with + member _.ModuleName = "TestModule" + member _.ModuleDuration = TimeSpan.Zero + member _.ModuleStart = DateTimeOffset.UtcNow + member _.ModuleEnd = DateTimeOffset.UtcNow + member _.ModuleStatus = Status.Successful + member _.ModuleResultType = ModuleResultType.Success + member _.IsSuccess = true + member _.IsFailure = false + member _.IsSkipped = false + member _.ValueOrDefault = null + member _.ExceptionOrDefault = null + member _.SkipDecisionOrDefault = Unchecked.defaultof<_> + +type ModuleExecutionPhaseTests() = + inherit TestBase() + + [] + member _.Pending_WithNoDependencies_IsReadyToQueue() = async { + let pending = + ModuleExecutionPhase.Pending( + UnresolvedDependencies = ImmutableHashSet.Empty, + DependentModules = ImmutableList.Empty + ) + + do! check(Assert.That(pending.IsReadyToQueue).IsTrue()) + } + + [] + member _.Pending_WithDependencies_IsNotReadyToQueue() = async { + let pending = + ModuleExecutionPhase.Pending( + UnresolvedDependencies = ImmutableHashSet.Create(typeof), + DependentModules = ImmutableList.Empty + ) + + do! check(Assert.That(pending.IsReadyToQueue).IsFalse()) + } + + [] + member _.Pending_RemovingDependency_CreatesNewInstance() = async { + let original = + ModuleExecutionPhase.Pending( + UnresolvedDependencies = ImmutableHashSet.Create(typeof, typeof), + DependentModules = ImmutableList.Empty + ) + + let updated = ModuleStateTransitions.RemoveDependency(original, typeof) + + do! check(Assert.That(updated.UnresolvedDependencies.Count = 1).IsTrue()) + do! check(Assert.That(original.UnresolvedDependencies.Count = 2).IsTrue()) + } + + [] + member _.Queued_HasCorrectTimestamps() = async { + let now = DateTimeOffset.UtcNow + + let queued = + ModuleExecutionPhase.Queued( + DependentModules = ImmutableList.Empty, + QueuedAt = now, + ReadyAt = now + ) + + do! check(Assert.That(queued.QueuedAt = now).IsTrue()) + do! check(Assert.That(queued.ReadyAt = now).IsTrue()) + } + + [] + member _.Running_HasStartedAt() = async { + let now = DateTimeOffset.UtcNow + use cts = new System.Threading.CancellationTokenSource() + + let running = + ModuleExecutionPhase.Running( + DependentModules = ImmutableList.Empty, + StartedAt = now, + QueuedAt = now.AddSeconds(-1), + CancellationSource = cts + ) + + do! check(Assert.That(running.StartedAt = now).IsTrue()) + do! check(Assert.That(obj.ReferenceEquals(running.CancellationSource, cts)).IsTrue()) + } + + [] + member _.Completed_CalculatesDuration() = async { + let startTime = DateTimeOffset.UtcNow + let endTime = startTime.AddSeconds(5) + + let completed = + ModuleExecutionPhase.Completed( + DependentModules = ImmutableList.Empty, + StartedAt = startTime, + CompletedAt = endTime, + Result = TestModuleResult() + ) + + do! check(Assert.That(completed.Duration = TimeSpan.FromSeconds(5.0)).IsTrue()) + } + + [] + member _.Failed_CalculatesDuration() = async { + let startTime = DateTimeOffset.UtcNow + let failTime = startTime.AddSeconds(3) + let ex = InvalidOperationException("Test failure") + + let failed = + ModuleExecutionPhase.Failed( + DependentModules = ImmutableList.Empty, + StartedAt = startTime, + FailedAt = failTime, + Exception = ex, + Result = TestModuleResult() + ) + + do! check(Assert.That(failed.Duration = TimeSpan.FromSeconds(3.0)).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(failed.Exception.Message), "Test failure")) + } + + [] + member _.Skipped_HasSkipDecision() = async { + let skipDecision = SkipDecision.Skip("Test skip reason") + + let skipped = + ModuleExecutionPhase.Skipped( + DependentModules = ImmutableList.Empty, + SkippedAt = DateTimeOffset.UtcNow, + SkipDecision = skipDecision, + Result = TestModuleResult() + ) + + do! check(Assert.That(skipped.SkipDecision.ShouldSkip).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(skipped.SkipDecision.Reason), "Test skip reason")) + } + + [] + member _.Cancelled_PreservesPreviousPhase() = async { + let previousPhase = + ModuleExecutionPhase.Running( + DependentModules = ImmutableList.Empty, + StartedAt = DateTimeOffset.UtcNow, + QueuedAt = DateTimeOffset.UtcNow.AddSeconds(-1), + CancellationSource = new System.Threading.CancellationTokenSource() + ) + + let cancelled = + ModuleExecutionPhase.Cancelled( + DependentModules = ImmutableList.Empty, + CancelledAt = DateTimeOffset.UtcNow, + PreviousPhase = previousPhase + ) + + do! check(Assert.That(obj.ReferenceEquals(cancelled.PreviousPhase, previousPhase)).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/State/ModuleStateStoreTests.fs b/test/ModularPipelines.UnitTests.FSharp/State/ModuleStateStoreTests.fs new file mode 100644 index 0000000000..5f08563a40 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/State/ModuleStateStoreTests.fs @@ -0,0 +1,479 @@ +namespace ModularPipelines.UnitTests.FSharp.State + +open System +open System.Collections.Immutable +open System.Threading +open Microsoft.Extensions.Time.Testing +open ModularPipelines.Context +open ModularPipelines.Engine.State +open ModularPipelines.Enums +open ModularPipelines.Models +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type private StoreTestModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + System.Threading.Tasks.Task.FromResult(null) + +type private StoreTestModule2() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + System.Threading.Tasks.Task.FromResult(null) + +type private StoreTestModuleResult() = + interface IModuleResult with + member _.ModuleName = "TestModule" + member _.ModuleDuration = TimeSpan.Zero + member _.ModuleStart = DateTimeOffset.UtcNow + member _.ModuleEnd = DateTimeOffset.UtcNow + member _.ModuleStatus = Status.Successful + member _.ModuleResultType = ModuleResultType.Success + member _.IsSuccess = true + member _.IsFailure = false + member _.IsSkipped = false + member _.ValueOrDefault = null + member _.ExceptionOrDefault = null + member _.SkipDecisionOrDefault = Unchecked.defaultof<_> + +type ModuleStateStoreTests() = + inherit TestBase() + + let mutable _timeProvider: FakeTimeProvider = Unchecked.defaultof<_> + let mutable _store: ModuleStateStore = Unchecked.defaultof<_> + + [] + member _.Setup() = + _timeProvider <- FakeTimeProvider(DateTimeOffset.UtcNow) + _store <- ModuleStateStore(_timeProvider) + + [] + member _.RegisterModule_CreatesInitialPendingState() = async { + let m = StoreTestModule() + let moduleType = typeof + let dependencies = ImmutableHashSet.Create(typeof) + let dependents = ImmutableList.Create(typeof) + + let state = + _store.RegisterModule( + m, + moduleType, + dependencies, + dependents, + requiresSequentialExecution = false, + requiredLockKeys = Array.empty, + priority = ModulePriority.Normal, + executionType = ExecutionType.Default + ) + + do! check(Assert.That(LanguagePrimitives.PhysicalEquality state.Module m).IsTrue()) + do! check(Assert.That(state.ModuleType = moduleType).IsTrue()) + do! check(Assert.That(state.Phase).IsTypeOf()) + + let pending = state.Phase :?> ModuleExecutionPhase.Pending + do! check(Assert.That(pending.UnresolvedDependencies.Count = 1).IsTrue()) + do! check(Assert.That(pending.DependentModules.Count = 1).IsTrue()) + } + + [] + member _.RegisterModule_DuplicateRegistration_ThrowsException() = async { + let m = StoreTestModule() + let moduleType = typeof + + _store.RegisterModule( + m, + moduleType, + ImmutableHashSet.Empty, + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + let mutable threw = false + + try + _store.RegisterModule( + m, + moduleType, + ImmutableHashSet.Empty, + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + with :? InvalidOperationException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.GetState_ReturnsRegisteredState() = async { + let m = StoreTestModule() + + _store.RegisterModule( + m, + typeof, + ImmutableHashSet.Empty, + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + let state = _store.GetState(typeof) + + do! check(Assert.That(state).IsNotNull()) + do! check(Assert.That(LanguagePrimitives.PhysicalEquality state.Module m).IsTrue()) + } + + [] + member _.GetState_UnknownModule_ReturnsNull() = async { + let state = _store.GetState(typeof) + do! check(Assert.That(state).IsNull()) + } + + [] + member _.TransitionToQueued_FromPendingWithNoDependencies_Succeeds() = async { + _store.RegisterModule( + StoreTestModule(), + typeof, + ImmutableHashSet.Empty, + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + let result = _store.TransitionToQueued(typeof) + + do! check(Assert.That(result).IsNotNull()) + do! check(Assert.That(result.Phase).IsTypeOf()) + } + + [] + member _.TransitionToQueued_FromPendingWithDependencies_ReturnsNull() = async { + _store.RegisterModule( + StoreTestModule(), + typeof, + ImmutableHashSet.Create(typeof), + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + let result = _store.TransitionToQueued(typeof) + + do! check(Assert.That(result).IsNull()) + } + + [] + member _.TransitionToRunning_FromQueued_Succeeds() = async { + _store.RegisterModule( + StoreTestModule(), + typeof, + ImmutableHashSet.Empty, + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + _store.TransitionToQueued(typeof) |> ignore + use cts = new CancellationTokenSource() + + let result = _store.TransitionToRunning(typeof, cts) + + do! check(Assert.That(result).IsNotNull()) + do! check(Assert.That(result.Phase).IsTypeOf()) + } + + [] + member _.TransitionToCompleted_FromRunning_Succeeds() = async { + _store.RegisterModule( + StoreTestModule(), + typeof, + ImmutableHashSet.Empty, + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + _store.TransitionToQueued(typeof) |> ignore + _store.TransitionToRunning(typeof, new CancellationTokenSource()) |> ignore + + let moduleResult = StoreTestModuleResult() + let result = _store.TransitionToCompleted(typeof, moduleResult) + + do! check(Assert.That(result).IsNotNull()) + do! check(Assert.That(result.Phase).IsTypeOf()) + do! check(Assert.That(result.IsSuccessful).IsTrue()) + } + + [] + member _.TransitionToFailed_FromRunning_Succeeds() = async { + _store.RegisterModule( + StoreTestModule(), + typeof, + ImmutableHashSet.Empty, + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + _store.TransitionToQueued(typeof) |> ignore + _store.TransitionToRunning(typeof, new CancellationTokenSource()) |> ignore + + let ex = Exception("Test failure") + let moduleResult = StoreTestModuleResult() + let result = _store.TransitionToFailed(typeof, ex, moduleResult) + + do! check(Assert.That(result).IsNotNull()) + do! check(Assert.That(result.Phase).IsTypeOf()) + + let failed = result.Phase :?> ModuleExecutionPhase.Failed + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(failed.Exception.Message), "Test failure")) + } + + [] + member _.TransitionToSkipped_FromPending_Succeeds() = async { + _store.RegisterModule( + StoreTestModule(), + typeof, + ImmutableHashSet.Create(typeof), + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + let skipDecision = SkipDecision.Skip("Test skip") + let moduleResult = StoreTestModuleResult() + let result = _store.TransitionToSkipped(typeof, skipDecision, moduleResult) + + do! check(Assert.That(result).IsNotNull()) + do! check(Assert.That(result.Phase).IsTypeOf()) + } + + [] + member _.RevertToPending_FromQueued_Succeeds() = async { + _store.RegisterModule( + StoreTestModule(), + typeof, + ImmutableHashSet.Empty, + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + _store.TransitionToQueued(typeof) |> ignore + let result = _store.RevertToPending(typeof) + + do! check(Assert.That(result).IsNotNull()) + do! check(Assert.That(result.Phase).IsTypeOf()) + } + + [] + member _.ResolveDependency_RemovesDependencyFromPending() = async { + _store.RegisterModule( + StoreTestModule(), + typeof, + ImmutableHashSet.Create(typeof, typeof), + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + let result = _store.ResolveDependency(typeof, typeof) + + do! check(Assert.That(result).IsNotNull()) + let pending = result.Phase :?> ModuleExecutionPhase.Pending + do! check(Assert.That(pending.UnresolvedDependencies.Count = 1).IsTrue()) + do! check(Assert.That(pending.UnresolvedDependencies.Contains(typeof)).IsTrue()) + } + + [] + member _.ResolveDependency_AllResolved_BecomesReadyToQueue() = async { + _store.RegisterModule( + StoreTestModule(), + typeof, + ImmutableHashSet.Create(typeof), + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + let result = _store.ResolveDependency(typeof, typeof) + + do! check(Assert.That(result).IsNotNull()) + do! check(Assert.That(result.IsReadyToQueue).IsTrue()) + } + + [] + member _.GetReadyModules_ReturnsOnlyReadyPendingModules() = async { + _store.RegisterModule( + StoreTestModule(), + typeof, + ImmutableHashSet.Empty, + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + _store.RegisterModule( + StoreTestModule2(), + typeof, + ImmutableHashSet.Create(typeof), + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + let ready = _store.GetReadyModules() |> Seq.toList + + do! check(Assert.That(ready.Length = 1).IsTrue()) + do! check(Assert.That(ready.[0].ModuleType = typeof).IsTrue()) + } + + [] + member _.GetStateCounts_ReturnsCorrectCounts() = async { + _store.RegisterModule( + StoreTestModule(), + typeof, + ImmutableHashSet.Empty, + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + _store.RegisterModule( + StoreTestModule2(), + typeof, + ImmutableHashSet.Empty, + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + _store.TransitionToQueued(typeof) |> ignore + + let struct (pending, queued, running, completed) = _store.GetStateCounts() + + do! check(Assert.That((pending = 1)).IsTrue()) + do! check(Assert.That((queued = 1)).IsTrue()) + do! check(Assert.That((running = 0)).IsTrue()) + do! check(Assert.That((completed = 0)).IsTrue()) + } + + [] + member _.StateChanged_FiresOnTransition() = async { + let mutable eventFired = false + let mutable oldState: ModuleStateSnapshot = Unchecked.defaultof<_> + let mutable newState: ModuleStateSnapshot = Unchecked.defaultof<_> + + let handler = + Action(fun old n -> + eventFired <- true + oldState <- old + newState <- n) + + try + _store.add_StateChanged(handler) + + _store.RegisterModule( + StoreTestModule(), + typeof, + ImmutableHashSet.Empty, + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + _store.TransitionToQueued(typeof) |> ignore + finally + _store.remove_StateChanged(handler) + + do! check(Assert.That(eventFired).IsTrue()) + do! check(Assert.That(oldState.Phase).IsTypeOf()) + do! check(Assert.That(newState.Phase).IsTypeOf()) + } + + [] + member _.Status_ReturnsCorrectEnumForEachPhase() = async { + _store.RegisterModule( + StoreTestModule(), + typeof, + ImmutableHashSet.Empty, + ImmutableList.Empty, + false, + Array.empty, + ModulePriority.Normal, + ExecutionType.Default + ) + |> ignore + + let pendingState = _store.GetState(typeof) + do! check(Assert.That(pendingState.Status = Status.NotYetStarted).IsTrue()) + + _store.TransitionToQueued(typeof) |> ignore + let queuedState = _store.GetState(typeof) + do! check(Assert.That(queuedState.Status = Status.NotYetStarted).IsTrue()) + + _store.TransitionToRunning(typeof, new CancellationTokenSource()) |> ignore + let runningState = _store.GetState(typeof) + do! check(Assert.That(runningState.Status = Status.Processing).IsTrue()) + + _store.TransitionToCompleted(typeof, StoreTestModuleResult()) |> ignore + let completedState = _store.GetState(typeof) + do! check(Assert.That(completedState.Status = Status.Successful).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/State/ModuleStateTransitionsTests.fs b/test/ModularPipelines.UnitTests.FSharp/State/ModuleStateTransitionsTests.fs new file mode 100644 index 0000000000..a5f67a100f --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/State/ModuleStateTransitionsTests.fs @@ -0,0 +1,367 @@ +namespace ModularPipelines.UnitTests.FSharp.State + +open System +open System.Collections.Immutable +open System.Threading +open ModularPipelines.Engine.State +open ModularPipelines.Enums +open ModularPipelines.Models +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type private TransitionTestModuleResult() = + interface IModuleResult with + member _.ModuleName = "TestModule" + member _.ModuleDuration = TimeSpan.Zero + member _.ModuleStart = DateTimeOffset.UtcNow + member _.ModuleEnd = DateTimeOffset.UtcNow + member _.ModuleStatus = Status.Successful + member _.ModuleResultType = ModuleResultType.Success + member _.IsSuccess = true + member _.IsFailure = false + member _.IsSkipped = false + member _.ValueOrDefault = null + member _.ExceptionOrDefault = null + member _.SkipDecisionOrDefault = Unchecked.defaultof<_> + +type ModuleStateTransitionsTests() = + inherit TestBase() + + [] + member _.IsValidTransition_PendingToQueued_ReturnsTrue() = async { + let fromPhase = + ModuleExecutionPhase.Pending( + UnresolvedDependencies = ImmutableHashSet.Empty, + DependentModules = ImmutableList.Empty + ) + + let toPhase = + ModuleExecutionPhase.Queued( + DependentModules = ImmutableList.Empty, + QueuedAt = DateTimeOffset.UtcNow, + ReadyAt = DateTimeOffset.UtcNow + ) + + let result = ModuleStateTransitions.IsValidTransition(fromPhase, toPhase) + do! check(Assert.That(result).IsTrue()) + } + + [] + member _.IsValidTransition_QueuedToRunning_ReturnsTrue() = async { + let fromPhase = + ModuleExecutionPhase.Queued( + DependentModules = ImmutableList.Empty, + QueuedAt = DateTimeOffset.UtcNow, + ReadyAt = DateTimeOffset.UtcNow + ) + + let toPhase = + ModuleExecutionPhase.Running( + DependentModules = ImmutableList.Empty, + StartedAt = DateTimeOffset.UtcNow, + QueuedAt = DateTimeOffset.UtcNow, + CancellationSource = CancellationTokenSource() + ) + + let result = ModuleStateTransitions.IsValidTransition(fromPhase, toPhase) + do! check(Assert.That(result).IsTrue()) + } + + [] + member _.IsValidTransition_QueuedToPending_ReturnsTrue() = async { + let fromPhase = + ModuleExecutionPhase.Queued( + DependentModules = ImmutableList.Empty, + QueuedAt = DateTimeOffset.UtcNow, + ReadyAt = DateTimeOffset.UtcNow + ) + + let toPhase = + ModuleExecutionPhase.Pending( + UnresolvedDependencies = ImmutableHashSet.Empty, + DependentModules = ImmutableList.Empty + ) + + let result = ModuleStateTransitions.IsValidTransition(fromPhase, toPhase) + do! check(Assert.That(result).IsTrue()) + } + + [] + member _.IsValidTransition_RunningToCompleted_ReturnsTrue() = async { + let fromPhase = + ModuleExecutionPhase.Running( + DependentModules = ImmutableList.Empty, + StartedAt = DateTimeOffset.UtcNow, + QueuedAt = DateTimeOffset.UtcNow, + CancellationSource = CancellationTokenSource() + ) + + let toPhase = + ModuleExecutionPhase.Completed( + DependentModules = ImmutableList.Empty, + StartedAt = DateTimeOffset.UtcNow, + CompletedAt = DateTimeOffset.UtcNow, + Result = TransitionTestModuleResult() + ) + + let result = ModuleStateTransitions.IsValidTransition(fromPhase, toPhase) + do! check(Assert.That(result).IsTrue()) + } + + [] + member _.IsValidTransition_RunningToFailed_ReturnsTrue() = async { + let fromPhase = + ModuleExecutionPhase.Running( + DependentModules = ImmutableList.Empty, + StartedAt = DateTimeOffset.UtcNow, + QueuedAt = DateTimeOffset.UtcNow, + CancellationSource = CancellationTokenSource() + ) + + let toPhase = + ModuleExecutionPhase.Failed( + DependentModules = ImmutableList.Empty, + StartedAt = DateTimeOffset.UtcNow, + FailedAt = DateTimeOffset.UtcNow, + Exception = Exception(), + Result = TransitionTestModuleResult() + ) + + let result = ModuleStateTransitions.IsValidTransition(fromPhase, toPhase) + do! check(Assert.That(result).IsTrue()) + } + + [] + member _.IsValidTransition_CompletedToAny_ReturnsFalse() = async { + let fromPhase = + ModuleExecutionPhase.Completed( + DependentModules = ImmutableList.Empty, + StartedAt = DateTimeOffset.UtcNow, + CompletedAt = DateTimeOffset.UtcNow, + Result = TransitionTestModuleResult() + ) + + let toPhase = + ModuleExecutionPhase.Running( + DependentModules = ImmutableList.Empty, + StartedAt = DateTimeOffset.UtcNow, + QueuedAt = DateTimeOffset.UtcNow, + CancellationSource = CancellationTokenSource() + ) + + let result = ModuleStateTransitions.IsValidTransition(fromPhase, toPhase) + do! check(Assert.That(result).IsFalse()) + } + + [] + member _.IsValidTransition_PendingToRunning_ReturnsFalse() = async { + let fromPhase = + ModuleExecutionPhase.Pending( + UnresolvedDependencies = ImmutableHashSet.Empty, + DependentModules = ImmutableList.Empty + ) + + let toPhase = + ModuleExecutionPhase.Running( + DependentModules = ImmutableList.Empty, + StartedAt = DateTimeOffset.UtcNow, + QueuedAt = DateTimeOffset.UtcNow, + CancellationSource = CancellationTokenSource() + ) + + let result = ModuleStateTransitions.IsValidTransition(fromPhase, toPhase) + do! check(Assert.That(result).IsFalse()) + } + + [] + member _.RemoveDependency_CreatesPendingWithDependencyRemoved() = async { + let pending = + ModuleExecutionPhase.Pending( + UnresolvedDependencies = ImmutableHashSet.Create(typeof, typeof), + DependentModules = ImmutableList.Empty + ) + + let result = ModuleStateTransitions.RemoveDependency(pending, typeof) + + do! check(Assert.That(result.UnresolvedDependencies.Count = 1).IsTrue()) + do! check(Assert.That(result.UnresolvedDependencies.Contains(typeof)).IsTrue()) + do! check(Assert.That(result.UnresolvedDependencies.Contains(typeof)).IsFalse()) + } + + [] + member _.ToQueued_CreateQueuedFromPending() = async { + let pending = + ModuleExecutionPhase.Pending( + UnresolvedDependencies = ImmutableHashSet.Empty, + DependentModules = ImmutableList.Create(typeof) + ) + + let now = DateTimeOffset.UtcNow + let result = ModuleStateTransitions.ToQueued(pending, now) + + do! check(Assert.That(result.QueuedAt = now).IsTrue()) + do! check(Assert.That(result.ReadyAt = now).IsTrue()) + do! check(Assert.That(result.DependentModules.Count = 1).IsTrue()) + } + + [] + member _.ToQueued_WithUnresolvedDependencies_ThrowsException() = async { + let pending = + ModuleExecutionPhase.Pending( + UnresolvedDependencies = ImmutableHashSet.Create(typeof), + DependentModules = ImmutableList.Empty + ) + + let mutable threw = false + + try + ModuleStateTransitions.ToQueued(pending, DateTimeOffset.UtcNow) |> ignore + with :? InvalidOperationException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.ToRunning_CreatesRunningFromQueued() = async { + let queuedAt = DateTimeOffset.UtcNow.AddSeconds(-1.0) + + let queued = + ModuleExecutionPhase.Queued( + DependentModules = ImmutableList.Create(typeof), + QueuedAt = queuedAt, + ReadyAt = queuedAt + ) + + let now = DateTimeOffset.UtcNow + use cts = new CancellationTokenSource() + + let result = ModuleStateTransitions.ToRunning(queued, now, cts) + + do! check(Assert.That(result.StartedAt = now).IsTrue()) + do! check(Assert.That(result.QueuedAt = queuedAt).IsTrue()) + do! check(Assert.That(obj.ReferenceEquals(result.CancellationSource, cts)).IsTrue()) + do! check(Assert.That(result.DependentModules.Count = 1).IsTrue()) + } + + [] + member _.ToPending_RevertQueuedToPending() = async { + let queued = + ModuleExecutionPhase.Queued( + DependentModules = ImmutableList.Create(typeof), + QueuedAt = DateTimeOffset.UtcNow, + ReadyAt = DateTimeOffset.UtcNow + ) + + let result = ModuleStateTransitions.ToPending(queued) + + do! check(Assert.That(result.UnresolvedDependencies.IsEmpty).IsTrue()) + do! check(Assert.That(result.DependentModules.Count = 1).IsTrue()) + } + + [] + member _.ToCompleted_CreatesCompletedFromRunning() = async { + let startedAt = DateTimeOffset.UtcNow.AddSeconds(-5.0) + + let running = + ModuleExecutionPhase.Running( + DependentModules = ImmutableList.Empty, + StartedAt = startedAt, + QueuedAt = startedAt.AddSeconds(-1.0), + CancellationSource = CancellationTokenSource() + ) + + let completedAt = DateTimeOffset.UtcNow + let moduleResult = TransitionTestModuleResult() + + let result = ModuleStateTransitions.ToCompleted(running, completedAt, moduleResult) + + do! check(Assert.That(result.StartedAt = startedAt).IsTrue()) + do! check(Assert.That(result.CompletedAt = completedAt).IsTrue()) + do! check(Assert.That(obj.ReferenceEquals(result.Result, moduleResult)).IsTrue()) + do! check(Assert.That(result.Duration = (completedAt - startedAt)).IsTrue()) + } + + [] + member _.ToFailed_CreatesFailedFromRunning() = async { + let startedAt = DateTimeOffset.UtcNow.AddSeconds(-3.0) + + let running = + ModuleExecutionPhase.Running( + DependentModules = ImmutableList.Empty, + StartedAt = startedAt, + QueuedAt = startedAt.AddSeconds(-1.0), + CancellationSource = CancellationTokenSource() + ) + + let failedAt = DateTimeOffset.UtcNow + let exnValue = InvalidOperationException("Test") + let moduleResult = TransitionTestModuleResult() + + let result = ModuleStateTransitions.ToFailed(running, failedAt, exnValue, moduleResult) + + do! check(Assert.That(result.StartedAt = startedAt).IsTrue()) + do! check(Assert.That(result.FailedAt = failedAt).IsTrue()) + do! check(Assert.That(obj.ReferenceEquals(result.Exception, exnValue)).IsTrue()) + do! check(Assert.That(obj.ReferenceEquals(result.Result, moduleResult)).IsTrue()) + } + + [] + member _.IsTerminal_ReturnsTrueForTerminalStates() = async { + let completed = + ModuleExecutionPhase.Completed( + DependentModules = ImmutableList.Empty, + StartedAt = DateTimeOffset.UtcNow, + CompletedAt = DateTimeOffset.UtcNow, + Result = TransitionTestModuleResult() + ) + + let failed = + ModuleExecutionPhase.Failed( + DependentModules = ImmutableList.Empty, + StartedAt = DateTimeOffset.UtcNow, + FailedAt = DateTimeOffset.UtcNow, + Exception = Exception(), + Result = TransitionTestModuleResult() + ) + + do! check(Assert.That(ModuleStateTransitions.IsTerminal(completed)).IsTrue()) + do! check(Assert.That(ModuleStateTransitions.IsTerminal(failed)).IsTrue()) + } + + [] + member _.IsTerminal_ReturnsFalseForNonTerminalStates() = async { + let pending = + ModuleExecutionPhase.Pending( + UnresolvedDependencies = ImmutableHashSet.Empty, + DependentModules = ImmutableList.Empty + ) + + let running = + ModuleExecutionPhase.Running( + DependentModules = ImmutableList.Empty, + StartedAt = DateTimeOffset.UtcNow, + QueuedAt = DateTimeOffset.UtcNow, + CancellationSource = CancellationTokenSource() + ) + + do! check(Assert.That(ModuleStateTransitions.IsTerminal(pending)).IsFalse()) + do! check(Assert.That(ModuleStateTransitions.IsTerminal(running)).IsFalse()) + } + + [] + member _.GetDependentModules_ReturnsCorrectListForAllPhases() = async { + let dependents = ImmutableList.Create(typeof, typeof) + + let pending = + ModuleExecutionPhase.Pending( + UnresolvedDependencies = ImmutableHashSet.Empty, + DependentModules = dependents + ) + + let result = ModuleStateTransitions.GetDependentModules(pending) + do! check(Assert.That(result.Count = 2).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Validation/ValidationInterfaceTests.fs b/test/ModularPipelines.UnitTests.FSharp/Validation/ValidationInterfaceTests.fs new file mode 100644 index 0000000000..df5baffea5 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Validation/ValidationInterfaceTests.fs @@ -0,0 +1,36 @@ +namespace ModularPipelines.UnitTests.FSharp.Validation + +open System.Reflection +open ModularPipelines.Context +open ModularPipelines.Validation +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type ValidationInterfaceTests() = + [] + member _.IPipelineValidationService_ShouldBeInternal() = async { + let assembly = typeof.Assembly + let iface = assembly.GetType("ModularPipelines.Validation.IPipelineValidationService") + + do! check(Assert.That(iface).IsNotNull()) + do! check(Assert.That(iface.IsPublic).IsFalse()) + } + + [] + member _.IPipelineValidator_ShouldRemainPublic() = async { + let validatorType = typeof + do! check(Assert.That(validatorType.IsPublic).IsTrue()) + } + + [] + member _.IPipelineValidator_ShouldHaveOrderAndValidateMembers() = async { + let validatorType = typeof + + let orderProperty = validatorType.GetProperty("Order") + do! check(Assert.That(orderProperty).IsNotNull()) + + let validateMethod = validatorType.GetMethod("Validate") + do! check(Assert.That(validateMethod).IsNotNull()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Validation/ValidationTests.fs b/test/ModularPipelines.UnitTests.FSharp/Validation/ValidationTests.fs new file mode 100644 index 0000000000..7fcf3c2f51 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Validation/ValidationTests.fs @@ -0,0 +1,322 @@ +namespace ModularPipelines.UnitTests.FSharp.Validation + +open System +open System.Linq +open ModularPipelines +open ModularPipelines.Context +open ModularPipelines.Exceptions +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.Validation +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type private SimpleModule() = + inherit Module() + override _.ExecuteAsync(_, _) = + System.Threading.Tasks.Task.FromResult(null) + +type private AnotherModule() = + inherit Module() + override _.ExecuteAsync(_, _) = + System.Threading.Tasks.Task.FromResult(42) + +[)>] +type private SelfReferencingModule() = + inherit Module() + override _.ExecuteAsync(_, _) = + System.Threading.Tasks.Task.FromResult("success") + +[)>] +type private ModuleA() = + inherit Module() + override _.ExecuteAsync(_, _) = System.Threading.Tasks.Task.FromResult(null) + +and [)>] ModuleB() = + inherit Module() + override _.ExecuteAsync(_, _) = System.Threading.Tasks.Task.FromResult(null) + +and [)>] ModuleC() = + inherit Module() + override _.ExecuteAsync(_, _) = System.Threading.Tasks.Task.FromResult(null) + +type private MissingModule() = + inherit Module() + override _.ExecuteAsync(_, _) = System.Threading.Tasks.Task.FromResult(null) + +[, Optional = false)>] +type private ModuleWithMissingDep() = + inherit Module() + override _.ExecuteAsync(_, _) = System.Threading.Tasks.Task.FromResult(null) + +[, Optional = true)>] +type private ModuleWithOptionalDep() = + inherit Module() + override _.ExecuteAsync(_, _) = System.Threading.Tasks.Task.FromResult(null) + +[] +module private ValidationTestHelpers = + let isBaseException<'T when 'T :> exn> (ex: exn) = + match ex with + | :? AggregateException as aggregate -> aggregate.GetBaseException() :? 'T + | _ -> ex :? 'T + +type ValidationTests() = + [] + member _.ValidateAsync_WithValidConfiguration_ReturnsNoErrors() = async { + let builder = Pipeline.CreateBuilder() + builder.Services.AddModule().AddModule() |> ignore + + let! result = builder.ValidateAsync() |> Async.AwaitTask + + do! check(Assert.That(result.IsValid).IsTrue()) + do! check(Assert.That(result.HasErrors).IsFalse()) + do! check(Assert.That(result.Errors.Count = 0).IsTrue()) + } + + [] + member _.ValidateAsync_WithNoModules_ReturnsError() = async { + let builder = Pipeline.CreateBuilder() + + let! result = builder.ValidateAsync() |> Async.AwaitTask + + do! check(Assert.That(result.HasErrors).IsTrue()) + + do! check( + Assert.That(result.Errors.Any(fun e -> e.Category = ValidationErrorCategory.ModuleConfiguration)) + .IsTrue() + ) + } + + [] + member _.BuildAsync_WithValidConfiguration_ReturnsPipeline() = async { + let builder = Pipeline.CreateBuilder() + builder.Services.AddModule() |> ignore + + let! pipeline = builder.BuildAsync() |> Async.AwaitTask + + do! check(Assert.That(pipeline).IsNotNull()) + do! check(Assert.That(pipeline.Services).IsNotNull()) + + do! pipeline.DisposeAsync().AsTask() |> Async.AwaitTask + } + + [] + member _.BuildAsync_WithNoModules_ThrowsValidationException() = async { + let builder = Pipeline.CreateBuilder() + + let mutable threw = false + + try + let! _ = builder.BuildAsync() |> Async.AwaitTask + () + with ex when isBaseException ex -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.ValidateAsync_WithMissingRequiredDependency_ReturnsNoError_BecauseAutoRegistered() = async { + let builder = Pipeline.CreateBuilder() + builder.Services.AddModule() |> ignore + + let! result = builder.ValidateAsync() |> Async.AwaitTask + + let hasDependencyError = + result.Errors.Any(fun e -> + e.Category = ValidationErrorCategory.Dependency + && e.Message.Contains("MissingModule")) + + do! check(Assert.That(hasDependencyError).IsFalse()) + } + + [] + member _.ValidateAsync_WithOptionalMissingDependency_ReturnsNoError() = async { + let builder = Pipeline.CreateBuilder() + builder.Services.AddModule() |> ignore + + let! result = builder.ValidateAsync() |> Async.AwaitTask + + let hasDependencyError = + result.Errors.Any(fun e -> + e.Category = ValidationErrorCategory.Dependency + && e.Message.Contains("MissingModule")) + + do! check(Assert.That(hasDependencyError).IsFalse()) + } + + [] + member _.RunAsync_AfterBuildAsync_ExecutesPipeline() = async { + let builder = Pipeline.CreateBuilder() + builder.Services.AddModule() |> ignore + + let! pipeline = builder.BuildAsync() |> Async.AwaitTask + let! summary = pipeline.RunAsync() |> Async.AwaitTask + + do! check(Assert.That(summary).IsNotNull()) + do! check(Assert.That>(summary.Modules).IsNotNull()) + + do! pipeline.DisposeAsync().AsTask() |> Async.AwaitTask + } + + [] + member _.ValidationResult_WithError_HasErrorsIsTrue() = async { + let error = ValidationError(ValidationErrorCategory.Options, "Test error") + let result = ValidationResult.WithError(error) + + do! check(Assert.That(result.HasErrors).IsTrue()) + do! check(Assert.That(result.IsValid).IsFalse()) + do! check(Assert.That(result.Errors.Count = 1).IsTrue()) + } + + [] + member _.ValidationResult_Success_IsValid() = async { + let result = ValidationResult.Success() + + do! check(Assert.That(result.HasErrors).IsFalse()) + do! check(Assert.That(result.IsValid).IsTrue()) + } + + [] + member _.ValidationResult_Merge_CombinesErrors() = async { + let error1 = ValidationError(ValidationErrorCategory.Options, "Error 1") + let error2 = ValidationError(ValidationErrorCategory.Dependency, "Error 2") + let result1 = ValidationResult.WithError(error1) + let result2 = ValidationResult.WithError(error2) + + result1.Merge(result2) + + do! check(Assert.That(result1.Errors.Count = 2).IsTrue()) + } + + [] + member _.ValidationError_ToString_IncludesCategory() = async { + let error = ValidationError(ValidationErrorCategory.Dependency, "Test message") + let str = error.ToString() + + do! check(Assert.That(str.Contains("Dependency")).IsTrue()) + do! check(Assert.That(str.Contains("Test message")).IsTrue()) + } + + [] + member _.ValidationError_ToString_WithSourceType_IncludesTypeName() = async { + let error = ValidationError(ValidationErrorCategory.ModuleConfiguration, "Test message", typeof) + let str = error.ToString() + + do! check(Assert.That(str.Contains("SimpleModule")).IsTrue()) + } + + [] + member _.ValidateAsync_WithNegativeRetryCount_ReturnsError() = async { + let builder = Pipeline.CreateBuilder() + builder.Services.AddModule() |> ignore + builder.Options.DefaultRetryCount <- -1 + + let! result = builder.ValidateAsync() |> Async.AwaitTask + + do! check(Assert.That(result.HasErrors).IsTrue()) + + do! check( + Assert.That( + result.Errors.Any(fun e -> + e.Category = ValidationErrorCategory.Options + && e.Message.Contains("DefaultRetryCount")) + ) + .IsTrue() + ) + } + + [] + member _.ValidateAsync_WithConflictingCategories_ReturnsError() = async { + let builder = Pipeline.CreateBuilder() + builder.Services.AddModule() |> ignore + builder.Options.RunOnlyCategories <- ResizeArray([ "Category1" ]) + builder.Options.IgnoreCategories <- ResizeArray([ "Category1" ]) + + let! result = builder.ValidateAsync() |> Async.AwaitTask + + do! check(Assert.That(result.HasErrors).IsTrue()) + + do! check( + Assert.That( + result.Errors.Any(fun e -> + e.Category = ValidationErrorCategory.Options + && e.Message.Contains("Category1")) + ) + .IsTrue() + ) + } + + [] + member _.ValidateAsync_WithSelfReferencingModule_ReturnsError() = async { + let builder = Pipeline.CreateBuilder() + builder.Services.AddModule() |> ignore + + let! result = builder.ValidateAsync() |> Async.AwaitTask + + do! check(Assert.That(result.HasErrors).IsTrue()) + + do! check( + Assert.That( + result.Errors.Any(fun e -> + e.Category = ValidationErrorCategory.Dependency + && e.Message.Contains("SelfReferencingModule") + && e.Message.Contains("cannot reference itself")) + ) + .IsTrue() + ) + } + + [] + member _.ValidateAsync_WithCircularDependency_ReturnsError() = async { + let builder = Pipeline.CreateBuilder() + builder.Services.AddModule().AddModule().AddModule() |> ignore + + let! result = builder.ValidateAsync() |> Async.AwaitTask + + do! check(Assert.That(result.HasErrors).IsTrue()) + + do! check( + Assert.That( + result.Errors.Any(fun e -> + e.Category = ValidationErrorCategory.Dependency + && (e.Message.Contains("Circular dependency") || e.Message.Contains("Dependency collision"))) + ) + .IsTrue() + ) + } + + [] + member _.BuildAsync_WithSelfReferencingModule_ThrowsValidationException() = async { + let builder = Pipeline.CreateBuilder() + builder.Services.AddModule() |> ignore + + let mutable threw = false + + try + let! _ = builder.BuildAsync() |> Async.AwaitTask + () + with ex when isBaseException ex -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.BuildAsync_WithCircularDependency_ThrowsValidationException() = async { + let builder = Pipeline.CreateBuilder() + builder.Services.AddModule().AddModule().AddModule() |> ignore + + let mutable threw = false + + try + let! _ = builder.BuildAsync() |> Async.AwaitTask + () + with ex when isBaseException ex -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } From b20103e75575f752e3ef1c7e75f32df7b7ee301f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 15:01:12 +1000 Subject: [PATCH 12/14] Replace wrapper-based remaining test migration and continue native F# test port fixes (#2) * Generate F# wrappers for remaining unit tests Agent-Logs-Url: https://github.com/licon4812/ModularPipelines/sessions/114fa639-5aec-4441-ae7b-fcb8805d5cf1 Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * Revert "Generate F# wrappers for remaining unit tests" This reverts commit c67143b9ee018d18cc34ed9105377c5fc48a3be5. Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * WIP generated remaining F# test wrappers Agent-Logs-Url: https://github.com/licon4812/ModularPipelines/sessions/114fa639-5aec-4441-ae7b-fcb8805d5cf1 Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * Remove wrapper infrastructure from F# test project Agent-Logs-Url: https://github.com/licon4812/ModularPipelines/sessions/f3923578-41dc-4b47-a511-c38bd6c1a1e3 Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * Fix F# test project build regressions Agent-Logs-Url: https://github.com/licon4812/ModularPipelines/sessions/ad0e9a0f-5d99-4bf8-af61-f7294d9e6656 Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * Restore native F# tests and fix many compile errors Agent-Logs-Url: https://github.com/licon4812/ModularPipelines/sessions/4701a5ec-874f-499d-9b7c-13fd34d69e45 Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> * Fixed build errors * More build fixes --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: licon4812 <32421608+licon4812@users.noreply.github.com> Co-authored-by: James Alickolli --- .../Builders/CommandBuilderBaseTests.fs | 466 +++++++++++++++ .../Builders/DotNetBuildBuilderTests.fs | 354 +++++++++++ .../Commands/CommandLoggerTests.fs | 212 +++++++ .../Commands/CommandParserTests.fs | 296 ++++++++++ .../Configuration/ModuleConfigurationTests.fs | 421 +++++++++++++ .../Configuration/ModuleConfigureTests.fs | 60 ++ .../OutputCoordinatorDeferredFlushTests.fs | 72 +++ .../Context/CommandLineBuilderTests.fs | 136 +++++ .../Context/ContextExtensionsTests.fs | 122 ++++ .../Context/ContextHierarchyTests.fs | 60 ++ .../Context/GitInformationTests.fs | 22 + .../Context/HttpTests.fs | 154 +++++ .../Context/InterfaceVisibilityTests.fs | 74 +++ .../CategoryFilterDependencyTests.fs | 116 ++++ .../CircularDependencyDetectionTests.fs | 203 +++++++ .../DependsOnAllInheritingFromTests.fs | 154 +++++ ...ependsOnModulesInCategoryAttributeTests.fs | 95 +++ ...ndsOnModulesWithAttributeAttributeTests.fs | 110 ++++ .../DependsOnModulesWithTagAttributeTests.fs | 95 +++ .../Dependencies/DependsOnTests.fs | 242 ++++++++ .../Dependencies/DirectCollisionTests.fs | 47 ++ .../DynamicDependencyDeclarationTests.fs | 464 +++++++++++++++ .../FlexibleDependencyIntegrationTests.fs | 558 ++++++++++++++++++ .../Dependencies/MockDependencyContext.fs | 53 ++ .../ModuleCategoryAttributeTests.fs | 52 ++ .../ModuleNotRegisteredExceptionTests.fs | 89 +++ .../Dependencies/ModuleTagAttributeTests.fs | 64 ++ .../Dependencies/NestedCollisionTests.fs | 56 ++ .../OneWayDependenciesNonCollisionTests.fs | 51 ++ .../SingleTypeParameterGetModuleTests.fs | 185 ++++++ .../Dependencies/TimedDependencyTests.fs | 62 ++ .../Engine/BuildSystemDetectorTests.fs | 72 +++ .../Engine/DependencyInjectionTests.fs | 66 +++ .../Engine/DependencyTreeFormatterTests.fs | 157 +++++ .../Engine/DisposerTests.fs | 45 ++ .../Engine/MetricsCollectorTests.fs | 190 ++++++ .../Execution/AlwaysRunTests.fs | 91 +++ .../Execution/AsyncDisposableModuleTests.fs | 46 ++ .../Execution/ComposableModuleTests.fs | 145 +++++ .../Execution/ConcurrencyOptionsTests.fs | 90 +++ .../Execution/DisposableModuleTests.fs | 41 ++ .../Execution/EngineCancellationTokenTests.fs | 135 +++++ .../Execution/ExecutionHintTests.fs | 162 +++++ .../NotInParallelTestsWithConstraintKeys.fs | 54 ++ ...ParallelTestsWithMultipleConstraintKeys.fs | 54 ++ .../Extensions/FolderExtensions.fs | 25 + .../Helpers/SerializationTestModels.fs | 45 ++ .../Logging/StringLogger.fs | 17 + .../ModularPipelines.UnitTests.FSharp.fsproj | 16 + .../Extensions/FolderExtensions.cs | 4 +- 50 files changed, 6598 insertions(+), 2 deletions(-) create mode 100644 test/ModularPipelines.UnitTests.FSharp/Builders/CommandBuilderBaseTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Builders/DotNetBuildBuilderTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Commands/CommandLoggerTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Commands/CommandParserTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Configuration/ModuleConfigurationTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Configuration/ModuleConfigureTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Console/OutputCoordinatorDeferredFlushTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Context/CommandLineBuilderTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Context/ContextExtensionsTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Context/ContextHierarchyTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Context/GitInformationTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Context/HttpTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Context/InterfaceVisibilityTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/CategoryFilterDependencyTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/CircularDependencyDetectionTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnAllInheritingFromTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnModulesInCategoryAttributeTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnModulesWithAttributeAttributeTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnModulesWithTagAttributeTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/DirectCollisionTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/DynamicDependencyDeclarationTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/FlexibleDependencyIntegrationTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/MockDependencyContext.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/ModuleCategoryAttributeTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/ModuleNotRegisteredExceptionTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/ModuleTagAttributeTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/NestedCollisionTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/OneWayDependenciesNonCollisionTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/SingleTypeParameterGetModuleTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Dependencies/TimedDependencyTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Engine/BuildSystemDetectorTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Engine/DependencyInjectionTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Engine/DependencyTreeFormatterTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Engine/DisposerTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Engine/MetricsCollectorTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Execution/AlwaysRunTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Execution/AsyncDisposableModuleTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Execution/ComposableModuleTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Execution/ConcurrencyOptionsTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Execution/DisposableModuleTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Execution/EngineCancellationTokenTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Execution/ExecutionHintTests.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Execution/NotInParallelTestsWithConstraintKeys.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Execution/NotInParallelTestsWithMultipleConstraintKeys.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Extensions/FolderExtensions.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Helpers/SerializationTestModels.fs create mode 100644 test/ModularPipelines.UnitTests.FSharp/Logging/StringLogger.fs diff --git a/test/ModularPipelines.UnitTests.FSharp/Builders/CommandBuilderBaseTests.fs b/test/ModularPipelines.UnitTests.FSharp/Builders/CommandBuilderBaseTests.fs new file mode 100644 index 0000000000..851653a23b --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Builders/CommandBuilderBaseTests.fs @@ -0,0 +1,466 @@ +namespace ModularPipelines.UnitTests.FSharp.Builders + +open System +open System.Collections.Generic +open System.Threading +open ModularPipelines.Attributes +open ModularPipelines.Builders +open ModularPipelines.Context +open ModularPipelines.Models +open ModularPipelines.Options +open ModularPipelines.TestHelpers +open Moq +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +[] +[] +type private TestToolOptions() = + inherit CommandLineToolOptions() + + [] + member val Configuration: string = Unchecked.defaultof<_> with get, set + + [] + member val Framework: string = Unchecked.defaultof<_> with get, set + + [] + member val NoRestore: Nullable = Nullable() with get, set + + override this.``$``() = + let clonedOptions = TestToolOptions() + clonedOptions.Configuration <- this.Configuration + clonedOptions.Framework <- this.Framework + clonedOptions.NoRestore <- this.NoRestore + clonedOptions :> CommandLineToolOptions + +module private CommandBuilderBaseTestHelpers = + let cloneTestToolOptions (options: TestToolOptions) = + let clonedOptions = TestToolOptions() + clonedOptions.Configuration <- options.Configuration + clonedOptions.Framework <- options.Framework + clonedOptions.NoRestore <- options.NoRestore + clonedOptions + + let createCommandResult command workingDirectory = + CommandResult( + command, + workingDirectory, + "output", + "", + Dictionary(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + 0 + ) + + let createMockCommand command workingDirectory = + let mockCommand = Mock() + + mockCommand + .Setup(fun c -> + c.ExecuteCommandLineTool( + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .ReturnsAsync(createCommandResult command workingDirectory) + |> ignore + + mockCommand + +open CommandBuilderBaseTestHelpers + +type private TestToolBuilder(command: ICommand, ?initialOptions: TestToolOptions) = + inherit CommandBuilderBase(command, defaultArg initialOptions (TestToolOptions())) + + member private this.UpdateToolOptions(update: TestToolOptions -> unit) = + let options = cloneTestToolOptions this.ToolOptions + update options + this.ToolOptions <- options + this + + member this.WithConfiguration(configuration: string) = + this.UpdateToolOptions(fun options -> options.Configuration <- configuration) + + member this.WithFramework(framework: string) = + this.UpdateToolOptions(fun options -> options.Framework <- framework) + + member this.WithNoRestore(?noRestore: bool) = + this.UpdateToolOptions(fun options -> options.NoRestore <- Nullable(defaultArg noRestore true)) + +type CommandBuilderBaseTests() = + inherit TestBase() + + [] + member _.WithWorkingDirectory_SetsWorkingDirectory() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + + builder.WithWorkingDirectory("/test/directory") |> ignore + + let struct (_, execOptions) = builder.ToOptions() + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(execOptions.WorkingDirectory), "/test/directory")) + } + + [] + member _.WithTimeout_SetsTimeout() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + let timeout = TimeSpan.FromMinutes(5.0) + + builder.WithTimeout(timeout) |> ignore + + let struct (_, execOptions) = builder.ToOptions() + do! check(Assert.That(execOptions.ExecutionTimeout).IsEqualTo(timeout)) + } + + [] + member _.WithEnvironmentVariable_AddsVariable() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + + builder.WithEnvironmentVariable("MY_VAR", "my_value") |> ignore + + let struct (_, execOptions) = builder.ToOptions() + match execOptions.EnvironmentVariables with + | null -> failwith "Expected environment variables" + | environmentVariables -> + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(environmentVariables["MY_VAR"]), "my_value")) + } + + [] + member _.WithEnvironmentVariable_AddsMultipleVariables() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + + builder + .WithEnvironmentVariable("VAR1", "value1") + .WithEnvironmentVariable("VAR2", "value2") + |> ignore + + let struct (_, execOptions) = builder.ToOptions() + match execOptions.EnvironmentVariables with + | null -> failwith "Expected environment variables" + | environmentVariables -> + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(environmentVariables["VAR1"]), "value1")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(environmentVariables["VAR2"]), "value2")) + } + + [] + member _.WithEnvironmentVariables_AddsDictionary() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + let variables = Dictionary() + variables["VAR1"] <- "value1" + variables["VAR2"] <- "value2" + + builder.WithEnvironmentVariables(variables) |> ignore + + let struct (_, execOptions) = builder.ToOptions() + match execOptions.EnvironmentVariables with + | null -> failwith "Expected environment variables" + | environmentVariables -> + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(environmentVariables["VAR1"]), "value1")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(environmentVariables["VAR2"]), "value2")) + } + + [] + member _.WithSudo_EnablesSudo() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + + builder.WithSudo() |> ignore + + let struct (_, execOptions) = builder.ToOptions() + do! check(Assert.That(execOptions.Sudo).IsTrue()) + } + + [] + member _.WithSudo_DisablesSudo_WhenFalse() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + + builder.WithSudo(false) |> ignore + + let struct (_, execOptions) = builder.ToOptions() + do! check(Assert.That(execOptions.Sudo).IsFalse()) + } + + [] + member _.WithThrowOnError_EnablesThrowOnError() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + + builder.WithThrowOnError() |> ignore + + let struct (_, execOptions) = builder.ToOptions() + do! check(Assert.That(execOptions.ThrowOnNonZeroExitCode).IsTrue()) + } + + [] + member _.WithThrowOnError_DisablesThrowOnError_WhenFalse() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + + builder.WithThrowOnError(false) |> ignore + + let struct (_, execOptions) = builder.ToOptions() + do! check(Assert.That(execOptions.ThrowOnNonZeroExitCode).IsFalse()) + } + + [] + member _.WithGracefulShutdownTimeout_SetsTimeout() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + let timeout = TimeSpan.FromSeconds(60.0) + + builder.WithGracefulShutdownTimeout(timeout) |> ignore + + let struct (_, execOptions) = builder.ToOptions() + do! check(Assert.That(execOptions.GracefulShutdownTimeout = timeout).IsTrue()) + } + + [] + member _.WithLogging_SetsLoggingOptions() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + let loggingOptions = CommandLoggingOptions(Verbosity = CommandLogVerbosity.Diagnostic, ShowExitCode = true) + + builder.WithLogging(loggingOptions) |> ignore + + let struct (_, execOptions) = builder.ToOptions() + match execOptions.LogSettings with + | null -> failwith "Expected logging settings" + | logSettings -> + do! check(Assert.That(logSettings.Verbosity).IsEqualTo(CommandLogVerbosity.Diagnostic)) + do! check(Assert.That(logSettings.ShowExitCode).IsTrue()) + } + + [] + member _.WithLogging_ConfiguresUsingAction() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + + builder.WithLogging(Action(fun _ -> ())) |> ignore + + let struct (_, execOptions) = builder.ToOptions() + do! check(Assert.That(execOptions.LogSettings <> null).IsTrue()) + } + + [] + member _.ToolSpecificOption_SetsToolOptions() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + + builder.WithConfiguration("Release") |> ignore + + let struct (toolOptions, _) = builder.ToOptions() + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Configuration), "Release")) + } + + [] + member _.InitialOptions_UsesProvidedOptions() = async { + let mockCommand = Mock() + let initialOptions = TestToolOptions() + initialOptions.Configuration <- "Debug" + let builder = TestToolBuilder(mockCommand.Object, initialOptions) + + let struct (toolOptions, _) = builder.ToOptions() + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Configuration), "Debug")) + } + + [] + member _.InitialOptions_CanBeModified() = async { + let mockCommand = Mock() + let initialOptions = TestToolOptions() + initialOptions.Configuration <- "Debug" + let builder = TestToolBuilder(mockCommand.Object, initialOptions) + + builder.WithFramework("net8.0") |> ignore + + let struct (toolOptions, _) = builder.ToOptions() + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Configuration), "Debug")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Framework), "net8.0")) + } + + [] + member _.FluentChaining_SetsAllOptions() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + + builder + .WithConfiguration("Release") + .WithFramework("net8.0") + .WithNoRestore() + .WithTimeout(TimeSpan.FromMinutes(10.0)) + .WithWorkingDirectory("/project") + .WithEnvironmentVariable("CI", "true") + |> ignore + + let struct (toolOptions, execOptions) = builder.ToOptions() + + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Configuration), "Release")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Framework), "net8.0")) + do! check(Assert.That(toolOptions.NoRestore.HasValue && toolOptions.NoRestore.Value).IsTrue()) + do! check(Assert.That(execOptions.ExecutionTimeout).IsEqualTo(TimeSpan.FromMinutes(10.0))) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(execOptions.WorkingDirectory), "/project")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(execOptions.EnvironmentVariables["CI"]), "true")) + } + + [] + member _.FluentChaining_ReturnsSameBuilderInstance() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + + let result1 = builder.WithConfiguration("Release") + let result2 = result1.WithFramework("net8.0") + let result3 = result2.WithTimeout(TimeSpan.FromMinutes(5.0)) + + do! check(Assert.That(obj.ReferenceEquals(builder, result1)).IsTrue()) + do! check(Assert.That(obj.ReferenceEquals(result1, result2)).IsTrue()) + do! check(Assert.That(obj.ReferenceEquals(result2, result3)).IsTrue()) + } + + [] + member _.ExecuteAsync_CallsCommandExecuteWithOptions() = async { + let mockCommand = createMockCommand "testtool command" "/working/dir" + let builder = TestToolBuilder(mockCommand.Object) + let mutable capturedToolOptions = Unchecked.defaultof + let mutable callCount = 0 + + mockCommand + .Setup(fun c -> + c.ExecuteCommandLineTool( + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .Callback(Action(fun options _ _ -> + callCount <- callCount + 1 + capturedToolOptions <- options :?> TestToolOptions)) + .ReturnsAsync(createCommandResult "testtool command" "/working/dir") + |> ignore + + builder.WithConfiguration("Release") |> ignore + + let! result = builder.ExecuteAsync() |> Async.AwaitTask + let hasExpectedCallCount = callCount = 1 + let capturedToolOptionsExists = obj.ReferenceEquals(capturedToolOptions, null) = false + let configurationWasCaptured = capturedToolOptions.Configuration = "Release" + + do! check(Assert.That(result).IsNotNull()) + do! check(Assert.That(hasExpectedCallCount).IsTrue()) + do! check(Assert.That(capturedToolOptionsExists).IsTrue()) + do! check(Assert.That(configurationWasCaptured).IsTrue()) + } + + [] + member _.ExecuteAsync_PassesCancellationToken() = async { + let mockCommand = createMockCommand "testtool command" "/working/dir" + let builder = TestToolBuilder(mockCommand.Object) + let mutable capturedToken = CancellationToken.None + use cts = new CancellationTokenSource() + + mockCommand + .Setup(fun c -> + c.ExecuteCommandLineTool( + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .Callback(Action(fun _ _ cancellationToken -> + capturedToken <- cancellationToken)) + .ReturnsAsync(createCommandResult "testtool command" "/working/dir") + |> ignore + + do! builder.ExecuteAsync(cts.Token) |> Async.AwaitTask |> Async.Ignore + let sameToken = capturedToken = cts.Token + + do! check(Assert.That(sameToken).IsTrue()) + } + + [] + member _.ExecuteAsync_PassesExecutionOptions() = async { + let mockCommand = createMockCommand "testtool command" "/test/dir" + let builder = TestToolBuilder(mockCommand.Object) + let mutable capturedExecOptions = Unchecked.defaultof + + mockCommand + .Setup(fun c -> + c.ExecuteCommandLineTool( + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .Callback(Action(fun _ execOptions _ -> + capturedExecOptions <- execOptions)) + .ReturnsAsync(createCommandResult "testtool command" "/test/dir") + |> ignore + + builder + .WithWorkingDirectory("/test/dir") + .WithTimeout(TimeSpan.FromMinutes(5.0)) + |> ignore + + do! builder.ExecuteAsync() |> Async.AwaitTask |> Async.Ignore + let capturedExecOptionsExists = obj.ReferenceEquals(capturedExecOptions, null) = false + + do! check(Assert.That(capturedExecOptionsExists).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(capturedExecOptions.WorkingDirectory), "/test/dir")) + do! check(Assert.That(capturedExecOptions.ExecutionTimeout).IsEqualTo(TimeSpan.FromMinutes(5.0))) + } + + [] + member _.ToOptions_ReturnsBothOptionsTuple() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + + builder + .WithConfiguration("Release") + .WithWorkingDirectory("/project") + |> ignore + + let struct (toolOptions, execOptions) = builder.ToOptions() + + do! check(Assert.That(toolOptions).IsNotNull()) + do! check(Assert.That(execOptions).IsNotNull()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Configuration), "Release")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(execOptions.WorkingDirectory), "/project")) + } + + [] + member _.ToOptions_CanBeCalledMultipleTimes() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + + builder.WithConfiguration("Release") |> ignore + let struct (options1, _) = builder.ToOptions() + + builder.WithFramework("net8.0") |> ignore + let struct (options2, _) = builder.ToOptions() + + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(options1.Configuration), "Release")) + do! check(Assert.That(options1.Framework).IsNull()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(options2.Configuration), "Release")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(options2.Framework), "net8.0")) + } + + [] + member _.NonGenericInterface_CanBeUsedForChaining() = async { + let mockCommand = Mock() + let builder = TestToolBuilder(mockCommand.Object) + let nonGenericBuilder = builder :> ICommandBuilder + + nonGenericBuilder + .WithWorkingDirectory("/test") + .WithTimeout(TimeSpan.FromMinutes(1.0)) + |> ignore + + let struct (_, execOptions) = builder.ToOptions() + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(execOptions.WorkingDirectory), "/test")) + do! check(Assert.That(execOptions.ExecutionTimeout).IsEqualTo(TimeSpan.FromMinutes(1.0))) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Builders/DotNetBuildBuilderTests.fs b/test/ModularPipelines.UnitTests.FSharp/Builders/DotNetBuildBuilderTests.fs new file mode 100644 index 0000000000..900f622063 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Builders/DotNetBuildBuilderTests.fs @@ -0,0 +1,354 @@ +namespace ModularPipelines.UnitTests.FSharp.Builders + +open System +open System.Collections.Generic +open System.Linq +open System.Threading +open ModularPipelines.Context +open ModularPipelines.DotNet.Builders +open ModularPipelines.DotNet.Options +open ModularPipelines.Models +open ModularPipelines.Options +open ModularPipelines.TestHelpers +open Moq +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module private DotNetBuildBuilderTestHelpers = + let createDotNetBuildResult workingDirectory = + CommandResult( + "dotnet build", + workingDirectory, + "", + "", + Dictionary(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + 0 + ) + + let createDotNetBuildMockCommand() = + let mockCommand = Mock() + + mockCommand + .Setup(fun c -> + c.ExecuteCommandLineTool( + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .ReturnsAsync(createDotNetBuildResult "/working/dir") + |> ignore + + mockCommand + +open DotNetBuildBuilderTestHelpers + +type DotNetBuildBuilderTests() = + inherit TestBase() + + [] + member _.ForProject_SetsProjectPath() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + + builder.ForProject("MyProject.csproj") |> ignore + + let struct (toolOptions, _) = builder.ToOptions() + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.ProjectSolution), "MyProject.csproj")) + } + + [] + member _.WithConfiguration_SetsConfiguration() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + + builder.WithConfiguration("Release") |> ignore + + let struct (toolOptions, _) = builder.ToOptions() + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Configuration), "Release")) + } + + [] + member _.WithFramework_SetsFramework() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + + builder.WithFramework("net8.0") |> ignore + + let struct (toolOptions, _) = builder.ToOptions() + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Framework), "net8.0")) + } + + [] + member _.WithRuntime_SetsRuntime() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + + builder.WithRuntime("win-x64") |> ignore + + let struct (toolOptions, _) = builder.ToOptions() + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Runtime), "win-x64")) + } + + [] + member _.WithOutput_SetsOutput() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + + builder.WithOutput("/output/path") |> ignore + + let struct (toolOptions, _) = builder.ToOptions() + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Output), "/output/path")) + } + + [] + member _.WithNoRestore_EnablesNoRestore() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + + builder.WithNoRestore() |> ignore + + let struct (toolOptions, _) = builder.ToOptions() + do! check(Assert.That(toolOptions.NoRestore.HasValue && toolOptions.NoRestore.Value).IsTrue()) + } + + [] + member _.WithNoIncremental_EnablesNoIncremental() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + + builder.WithNoIncremental() |> ignore + + let struct (toolOptions, _) = builder.ToOptions() + do! check(Assert.That(toolOptions.NoIncremental.HasValue && toolOptions.NoIncremental.Value).IsTrue()) + } + + [] + member _.WithNoLogo_EnablesNoLogo() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + + builder.WithNoLogo() |> ignore + + let struct (toolOptions, _) = builder.ToOptions() + do! check(Assert.That(toolOptions.Nologo.HasValue && toolOptions.Nologo.Value).IsTrue()) + } + + [] + member _.WithProperty_AddsProperty() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + + builder.WithProperty("Version", "1.0.0") |> ignore + + let struct (toolOptions, _) = builder.ToOptions() + match toolOptions.Properties with + | null -> failwith "Expected properties" + | properties -> + do! check(Assert.That(properties.Count() = 1).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(properties.First().Key), "Version")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(properties.First().Value), "1.0.0")) + } + + [] + member _.WithProperty_AddsMultipleProperties() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + + builder + .WithProperty("Version", "1.0.0") + .WithProperty("AssemblyVersion", "1.0.0.0") + |> ignore + + let struct (toolOptions, _) = builder.ToOptions() + match toolOptions.Properties with + | null -> failwith "Expected properties" + | properties -> + do! check(Assert.That(properties.Count() = 2).IsTrue()) + } + + [] + member _.WithWorkingDirectory_SetsWorkingDirectory() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + + builder.WithWorkingDirectory("/project/dir") |> ignore + + let struct (_, execOptions) = builder.ToOptions() + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(execOptions.WorkingDirectory), "/project/dir")) + } + + [] + member _.WithTimeout_SetsTimeout() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + let timeout = TimeSpan.FromMinutes(30.0) + + builder.WithTimeout(timeout) |> ignore + + let struct (_, execOptions) = builder.ToOptions() + do! check(Assert.That(execOptions.ExecutionTimeout = timeout).IsTrue()) + } + + [] + member _.WithEnvironmentVariable_AddsVariable() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + + builder.WithEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", "1") |> ignore + + let struct (_, execOptions) = builder.ToOptions() + match execOptions.EnvironmentVariables with + | null -> failwith "Expected environment variables" + | environmentVariables -> + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(environmentVariables["DOTNET_CLI_TELEMETRY_OPTOUT"]), "1")) + } + + [] + member _.WithThrowOnError_SetsThrowOnError() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + + builder.WithThrowOnError(false) |> ignore + + let struct (_, execOptions) = builder.ToOptions() + do! check(Assert.That(execOptions.ThrowOnNonZeroExitCode).IsFalse()) + } + + [] + member _.FluentChaining_SetsAllOptions() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + + builder + .ForProject("MyProject.sln") + .WithConfiguration("Release") + .WithFramework("net8.0") + .WithNoRestore() + .WithNoLogo() + .WithProperty("Version", "2.0.0") + .WithWorkingDirectory("/project") + .WithTimeout(TimeSpan.FromMinutes(15.0)) + |> ignore + + let struct (toolOptions, execOptions) = builder.ToOptions() + + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.ProjectSolution), "MyProject.sln")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Configuration), "Release")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Framework), "net8.0")) + do! check(Assert.That(toolOptions.NoRestore.HasValue && toolOptions.NoRestore.Value).IsTrue()) + do! check(Assert.That(toolOptions.Nologo.HasValue && toolOptions.Nologo.Value).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Properties.First().Key), "Version")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(execOptions.WorkingDirectory), "/project")) + do! check(Assert.That(execOptions.ExecutionTimeout = TimeSpan.FromMinutes(15.0)).IsTrue()) + } + + [] + member _.FluentChaining_ReturnsSameBuilderInstance() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + + let result1 = builder.ForProject("test.csproj") + let result2 = result1.WithConfiguration("Release") + let result3 = result2.WithFramework("net8.0") + + do! check(Assert.That(obj.ReferenceEquals(builder, result1)).IsTrue()) + do! check(Assert.That(obj.ReferenceEquals(result1, result2)).IsTrue()) + do! check(Assert.That(obj.ReferenceEquals(result2, result3)).IsTrue()) + } + + [] + member _.ExecuteAsync_CallsCommandExecuteWithOptions() = async { + let mockCommand = createDotNetBuildMockCommand() + let builder = DotNetBuildBuilder(mockCommand.Object) + let mutable capturedToolOptions = Unchecked.defaultof + let mutable callCount = 0 + + mockCommand + .Setup(fun c -> + c.ExecuteCommandLineTool( + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .Callback(Action(fun options _ _ -> + callCount <- callCount + 1 + capturedToolOptions <- options :?> DotNetBuildOptions)) + .ReturnsAsync(createDotNetBuildResult "/working/dir") + |> ignore + + builder + .ForProject("MyProject.csproj") + .WithConfiguration("Release") + |> ignore + + let! result = builder.ExecuteAsync() |> Async.AwaitTask + let hasExpectedCallCount = callCount = 1 + let capturedToolOptionsExists = obj.ReferenceEquals(capturedToolOptions, null) = false + + do! check(Assert.That(result).IsNotNull()) + do! check(Assert.That(hasExpectedCallCount).IsTrue()) + do! check(Assert.That(capturedToolOptionsExists).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(capturedToolOptions.ProjectSolution), "MyProject.csproj")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(capturedToolOptions.Configuration), "Release")) + } + + [] + member _.ExecuteAsync_PassesExecutionOptions() = async { + let mockCommand = createDotNetBuildMockCommand() + let mutable capturedExecOptions = Unchecked.defaultof + + mockCommand + .Setup(fun c -> + c.ExecuteCommandLineTool( + It.IsAny(), + It.IsAny(), + It.IsAny() + )) + .Callback(Action(fun _ execOptions _ -> + capturedExecOptions <- execOptions)) + .ReturnsAsync(createDotNetBuildResult "/test/dir") + |> ignore + + let builder = DotNetBuildBuilder(mockCommand.Object) + + builder + .WithWorkingDirectory("/test/dir") + .WithTimeout(TimeSpan.FromMinutes(10.0)) + |> ignore + + do! builder.ExecuteAsync() |> Async.AwaitTask |> Async.Ignore + let capturedExecOptionsExists = obj.ReferenceEquals(capturedExecOptions, null) = false + + do! check(Assert.That(capturedExecOptionsExists).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(capturedExecOptions.WorkingDirectory), "/test/dir")) + do! check(Assert.That(capturedExecOptions.ExecutionTimeout = TimeSpan.FromMinutes(10.0)).IsTrue()) + } + + [] + member _.InitialOptions_UsesProvidedOptions() = async { + let mockCommand = createDotNetBuildMockCommand() + let initialOptions = DotNetBuildOptions(Configuration = "Debug", Framework = "net7.0") + let builder = DotNetBuildBuilder(mockCommand.Object, initialOptions) + + let struct (toolOptions, _) = builder.ToOptions() + + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Configuration), "Debug")) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Framework), "net7.0")) + } + + [] + member _.InitialOptions_CanBeOverridden() = async { + let mockCommand = createDotNetBuildMockCommand() + let initialOptions = DotNetBuildOptions(Configuration = "Debug") + let builder = DotNetBuildBuilder(mockCommand.Object, initialOptions) + + builder.WithConfiguration("Release") |> ignore + + let struct (toolOptions, _) = builder.ToOptions() + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(toolOptions.Configuration), "Release")) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Commands/CommandLoggerTests.fs b/test/ModularPipelines.UnitTests.FSharp/Commands/CommandLoggerTests.fs new file mode 100644 index 0000000000..f7426c6e76 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Commands/CommandLoggerTests.fs @@ -0,0 +1,212 @@ +namespace ModularPipelines.UnitTests.FSharp.Commands + +open System +open System.IO +open System.Text.RegularExpressions +open System.Threading.Tasks +open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Logging +open ModularPipelines.Context +open ModularPipelines.Options +open ModularPipelines.TestHelpers +open NReco.Logging.File +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open Vertical.SpectreLogger.Options + +type CommandLoggerTests() = + inherit TestBase() + + member private this.GetCommandWithFileLogger(file: string) : Task = + this.GetService( + Action(fun services -> + services.AddLogging(fun builder -> + services.Configure(fun (options: SpectreLoggerOptions) -> + options.MinimumLogLevel <- LogLevel.Information) + |> ignore + + services.Configure(fun (options: LoggerFilterOptions) -> + options.MinLevel <- LogLevel.Information) + |> ignore + + builder.AddFile(file) |> ignore) + |> ignore) + ) + + member private this.RunPowershellCommand(command: string, logInput: bool, logOutput: bool, logError: bool, logExitCode: bool, logDuration: bool) = task { + let file = Path.Combine(TestContext.WorkingDirectory, Guid.NewGuid().ToString("N") + ".txt") + + let! struct (commandService, host) = this.GetCommandWithFileLogger(file) + + let verbosity = + if not logInput && not logOutput && not logError && not logExitCode && not logDuration then + CommandLogVerbosity.Silent + else + CommandLogVerbosity.Normal + + let loggingOptions = + CommandLoggingOptions( + Verbosity = verbosity, + ShowCommandArguments = logInput, + ShowStandardOutput = logOutput, + ShowStandardError = logError, + ShowExitCode = logExitCode, + ShowExecutionTime = logDuration + ) + + let! _ = + commandService.ExecuteCommandLineTool( + PowershellScriptOptions(command), + CommandExecutionOptions(LogSettings = loggingOptions, ThrowOnNonZeroExitCode = false) + ) + + do! host.DisposeAsync().AsTask() + + return file + } + + member private this.RunPowershellCommandWithLoggingOptions(command: string, loggingOptions: CommandLoggingOptions) = task { + let file = Path.Combine(TestContext.WorkingDirectory, Guid.NewGuid().ToString("N") + ".txt") + + let! struct (commandService, host) = this.GetCommandWithFileLogger(file) + + let! _ = + commandService.ExecuteCommandLineTool( + PowershellScriptOptions(command), + CommandExecutionOptions(LogSettings = loggingOptions, ThrowOnNonZeroExitCode = false) + ) + + do! host.DisposeAsync().AsTask() + + return file + } + + [] + [] + member this.Logs_As_Expected_With_Options( + [] logInput: bool, + [] logOutput: bool, + [] logError: bool, + [] logExitCode: bool, + [] logDuration: bool + ) = async { + let! file = + this.RunPowershellCommand( + "echo Hello world!\nthrow \"Error!\"", + logInput, + logOutput, + logError, + logExitCode, + logDuration + ) + |> Async.AwaitTask + + let! logFile = File.ReadAllTextAsync(file) |> Async.AwaitTask + + if not logInput && not logOutput && not logError && not logDuration && not logExitCode then + do! check(Assert.That(logFile.Contains("INFO\t[ModularPipelines.Pipeline]")).IsFalse()) + else + do! check(Assert.That(logFile.Contains("INFO\t[ModularPipelines.Pipeline]")).IsTrue()) + + if logInput then + do! check(Assert.That(logFile.Contains($"{Environment.CurrentDirectory}> pwsh -Command \"echo Hello world!")).IsTrue()) + else + do! check(Assert.That(logFile.Contains($"{Environment.CurrentDirectory}> ********")).IsTrue()) + + if logOutput then + do! check(Assert.That(logFile.Contains("→") || logFile.Contains("↳")).IsTrue()) + + if logError then + do! check(Assert.That(logFile.Contains("✗")).IsTrue()) + + if logDuration then + do! check(Assert.That(Regex.IsMatch(logFile, @"\[\d+m?s")).IsTrue()) + + if logExitCode then + do! check(Assert.That(logFile.Contains("exit ")).IsTrue()) + } + + [] + member this.Silent_Verbosity_Logs_Nothing() = async { + let! file = + this.RunPowershellCommandWithLoggingOptions( + "echo Hello", + CommandLoggingOptions(Verbosity = CommandLogVerbosity.Silent) + ) + |> Async.AwaitTask + + let! logFile = File.ReadAllTextAsync(file) |> Async.AwaitTask + do! check(Assert.That(logFile.Contains($"{Environment.CurrentDirectory}>")).IsFalse()) + do! check(Assert.That(logFile.Contains("→")).IsFalse()) + do! check(Assert.That(logFile.Contains("↳")).IsFalse()) + do! check(Assert.That(logFile.Contains("exit ")).IsFalse()) + do! check(Assert.That(logFile.Contains("Working Directory:")).IsFalse()) + } + + [] + member this.Minimal_Verbosity_Logs_Only_Input() = async { + let! file = + this.RunPowershellCommandWithLoggingOptions( + "echo Hello", + CommandLoggingOptions(Verbosity = CommandLogVerbosity.Minimal) + ) + |> Async.AwaitTask + + let! logFile = File.ReadAllTextAsync(file) |> Async.AwaitTask + do! check(Assert.That(logFile.Contains($"{Environment.CurrentDirectory}>")).IsTrue()) + do! check(Assert.That(logFile.Contains("→")).IsFalse()) + do! check(Assert.That(logFile.Contains("↳")).IsFalse()) + do! check(Assert.That(logFile.Contains("exit ")).IsFalse()) + do! check(Assert.That(Regex.IsMatch(logFile, @"\[\d+m?s")).IsFalse()) + } + + [] + member this.Normal_Verbosity_Logs_Input_And_Output() = async { + let! file = + this.RunPowershellCommandWithLoggingOptions( + "echo Hello", + CommandLoggingOptions(Verbosity = CommandLogVerbosity.Normal) + ) + |> Async.AwaitTask + + let! logFile = File.ReadAllTextAsync(file) |> Async.AwaitTask + do! check(Assert.That(logFile.Contains($"{Environment.CurrentDirectory}>")).IsTrue()) + do! check(Assert.That(logFile.Contains("→")).IsTrue()) + do! check(Assert.That(logFile.Contains("exit ")).IsFalse()) + do! check(Assert.That(Regex.IsMatch(logFile, @"\[\d+m?s")).IsFalse()) + } + + [] + member this.Detailed_Verbosity_Logs_Input_Output_ExitCode_Duration() = async { + let! file = + this.RunPowershellCommandWithLoggingOptions( + "echo Hello", + CommandLoggingOptions(Verbosity = CommandLogVerbosity.Detailed) + ) + |> Async.AwaitTask + + let! logFile = File.ReadAllTextAsync(file) |> Async.AwaitTask + do! check(Assert.That(logFile.Contains($"{Environment.CurrentDirectory}>")).IsTrue()) + do! check(Assert.That(logFile.Contains("→")).IsTrue()) + do! check(Assert.That(logFile.Contains("exit ")).IsTrue()) + do! check(Assert.That(Regex.IsMatch(logFile, @"\[\d+m?s")).IsTrue()) + } + + [] + member this.Diagnostic_Verbosity_Logs_Everything_Including_WorkingDirectory() = async { + let! file = + this.RunPowershellCommandWithLoggingOptions( + "echo Hello", + CommandLoggingOptions(Verbosity = CommandLogVerbosity.Diagnostic) + ) + |> Async.AwaitTask + + let! logFile = File.ReadAllTextAsync(file) |> Async.AwaitTask + do! check(Assert.That(logFile.Contains($"{Environment.CurrentDirectory}>")).IsTrue()) + do! check(Assert.That(logFile.Contains("→")).IsTrue()) + do! check(Assert.That(logFile.Contains("exit ")).IsTrue()) + do! check(Assert.That(Regex.IsMatch(logFile, @"\[\d+m?s")).IsTrue()) + do! check(Assert.That(logFile.Contains("Working Directory:")).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Commands/CommandParserTests.fs b/test/ModularPipelines.UnitTests.FSharp/Commands/CommandParserTests.fs new file mode 100644 index 0000000000..e1e9b7d2c7 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Commands/CommandParserTests.fs @@ -0,0 +1,296 @@ +namespace ModularPipelines.UnitTests.FSharp.Commands + +open System +open System.Collections.Generic +open System.Threading +open ModularPipelines.Attributes +open ModularPipelines.Context +open ModularPipelines.DotNet.Options +open ModularPipelines.Models +open ModularPipelines.Options +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +[] +[] +type private MySuperSecretToolOptions() = + inherit CommandLineToolOptions() + + [] + member val BuildArgs: IEnumerable = null with get, set + + [] + member val Force = Nullable() with get, set + + [] + member val Verbosity: string = null with get, set + + [] + member val GracePeriod = Nullable() with get, set + + [] + member val SomeString: string = null with get, set + + [] + member val Filename: string array = null with get, set + + [] + member val Positional1: string = null with get, set + + [] + member val Positional2: string = null with get, set + + override this.``$``() = + let clonedOptions = MySuperSecretToolOptions() + clonedOptions.BuildArgs <- this.BuildArgs + clonedOptions.Force <- this.Force + clonedOptions.Verbosity <- this.Verbosity + clonedOptions.GracePeriod <- this.GracePeriod + clonedOptions.SomeString <- this.SomeString + clonedOptions.Filename <- this.Filename + clonedOptions.Positional1 <- this.Positional1 + clonedOptions.Positional2 <- this.Positional2 + clonedOptions :> CommandLineToolOptions + +[] +[] +type private PlaceholderToolOptions(package: string, project: string) = + inherit CommandLineToolOptions() + + [] + member val Project = project with get, set + + [] + member val Command = "package" with get, set + + [] + member val Package = package with get, set + + [] + member val Source: string = null with get, set + + override this.``$``() = + let clonedOptions = PlaceholderToolOptions(this.Package, this.Project) + clonedOptions.Command <- this.Command + clonedOptions.Source <- this.Source + clonedOptions :> CommandLineToolOptions + +[] +[] +type private PlaceholderToolOptions3() = + inherit CommandLineToolOptions() + + [] + member val Project: string = null with get, set + + [] + member val Source: string = null with get, set + + override this.``$``() = + let clonedOptions = PlaceholderToolOptions3() + clonedOptions.Project <- this.Project + clonedOptions.Source <- this.Source + clonedOptions :> CommandLineToolOptions + +type CommandParserTests() = + inherit TestBase() + + member private this.GetResult(options: CommandLineToolOptions) = + task { + let! command = this.GetService() + + let executionOptions = CommandExecutionOptions(InternalDryRun = true) + + return! command.ExecuteCommandLineTool(options, executionOptions, CancellationToken.None) + } + + [] + member this.Empty_Options_Parse_As_Expected() = async { + let! result = this.GetResult(MySuperSecretToolOptions()) |> Async.AwaitTask + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.CommandInput), "mysupersecrettool do this then that")) + } + + [] + member this.KeyValues_Parse_As_Expected() = async { + let options = MySuperSecretToolOptions() + options.BuildArgs <- + [| + KeyValue("Arg1", "Value1") + KeyValue("Arg2", "Value2") + KeyValue("Arg3", "Value3") + |] + + let! result = this.GetResult(options) |> Async.AwaitTask + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(result.CommandInput), + "mysupersecrettool do this then that --build-arg Arg1=Value1 --build-arg Arg2=Value2 --build-arg Arg3=Value3" + ) + ) + } + + [] + member this.Boolean_Switch_Parse_As_Expected_When_True() = async { + let options = MySuperSecretToolOptions() + options.Force <- Nullable true + + let! result = this.GetResult(options) |> Async.AwaitTask + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.CommandInput), "mysupersecrettool do this then that --force")) + } + + [] + member this.Boolean_Switch_Parse_As_Expected_When_Null() = async { + let options = MySuperSecretToolOptions() + options.Force <- Nullable() + + let! result = this.GetResult(options) |> Async.AwaitTask + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.CommandInput), "mysupersecrettool do this then that")) + } + + [] + member this.Boolean_Switch_Parse_As_Expected_When_False() = async { + let options = MySuperSecretToolOptions() + options.Force <- Nullable false + + let! result = this.GetResult(options) |> Async.AwaitTask + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.CommandInput), "mysupersecrettool do this then that")) + } + + [] + member this.String_Array_Switch_Parse_As_Expected() = async { + let options = MySuperSecretToolOptions() + options.Filename <- [| "file1.txt"; "foo.txt"; "bar.json" |] + + let! result = this.GetResult(options) |> Async.AwaitTask + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(result.CommandInput), + "mysupersecrettool do this then that --filename file1.txt --filename foo.txt --filename bar.json" + ) + ) + } + + [] + member this.String_Switch_Parse_As_Expected() = async { + let options = MySuperSecretToolOptions() + options.SomeString <- "Foo bar" + + let! result = this.GetResult(options) |> Async.AwaitTask + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(result.CommandInput), + "mysupersecrettool do this then that --some-string \"Foo bar\"" + ) + ) + } + + [] + member this.Int_Switch_Parse_As_Expected() = async { + let options = MySuperSecretToolOptions() + options.GracePeriod <- Nullable 123 + + let! result = this.GetResult(options) |> Async.AwaitTask + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.CommandInput), "mysupersecrettool do this then that --grace-period 123")) + } + + [] + member this.Enum_Value_Switch_Parse_As_Expected() = async { + let options = MySuperSecretToolOptions() + options.Verbosity <- "quiet" + + let! result = this.GetResult(options) |> Async.AwaitTask + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.CommandInput), "mysupersecrettool do this then that --verbosity quiet")) + } + + [] + member this.Positional_Parameter_Before_Switches_Parse_As_Expected() = async { + let options = MySuperSecretToolOptions() + options.SomeString <- "Foo bar" + options.Positional1 <- "MyFile.txt" + + let! result = this.GetResult(options) |> Async.AwaitTask + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(result.CommandInput), + "mysupersecrettool do this then that MyFile.txt --some-string \"Foo bar\"" + ) + ) + } + + [] + member this.Positional_Parameter_After_Switches_Parse_As_Expected() = async { + let options = MySuperSecretToolOptions() + options.SomeString <- "Foo bar" + options.Positional2 <- "MyFile.txt" + + let! result = this.GetResult(options) |> Async.AwaitTask + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(result.CommandInput), + "mysupersecrettool do this then that --some-string \"Foo bar\" MyFile.txt" + ) + ) + } + + [] + member this.Multiple_Positional_Arguments_With_Interleaved_Command() = async { + let options = PlaceholderToolOptions("ThisPackage", "MyProject.csproj") + options.Source <- "nuget.org" + + let! result = this.GetResult(options) |> Async.AwaitTask + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.CommandInput), "dotnet add MyProject.csproj package ThisPackage --source nuget.org")) + } + + [] + member this.Single_Positional_Argument_Immediately_After_Command() = async { + let options = PlaceholderToolOptions3() + options.Project <- "MyProject.csproj" + + let! result = this.GetResult(options) |> Async.AwaitTask + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.CommandInput), "dotnet add MyProject.csproj")) + } + + [] + member this.DotNet_Nuget_Delete_With_Two_Positional_Arguments() = async { + let options = DotNetNugetDeleteOptions() + options.PackageName <- "MyPackageName" + options.Version <- "1.0.0" + + let! result = this.GetResult(options) |> Async.AwaitTask + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.CommandInput), "dotnet nuget delete MyPackageName 1.0.0")) + } + + [] + member this.DotNet_Nuget_Delete_With_Source_Option() = async { + let options = DotNetNugetDeleteOptions() + options.PackageName <- "MyPackageName" + options.Version <- "1.0.0" + options.Source <- "https://api.nuget.org/v3/index.json" + + let! result = this.GetResult(options) |> Async.AwaitTask + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(result.CommandInput), + "dotnet nuget delete MyPackageName 1.0.0 --source https://api.nuget.org/v3/index.json" + ) + ) + } + + [] + member this.DotNet_Nuget_Delete_With_ApiKey_Option() = async { + let options = DotNetNugetDeleteOptions() + options.PackageName <- "MyPackageName" + options.Version <- "1.0.0" + options.ApiKey <- "my-secret-key" + + let! result = this.GetResult(options) |> Async.AwaitTask + do! check( + StringEqualsAssertionExtensions.IsEqualTo( + Assert.That(result.CommandInput), + "dotnet nuget delete MyPackageName 1.0.0 --api-key my-secret-key" + ) + ) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Configuration/ModuleConfigurationTests.fs b/test/ModularPipelines.UnitTests.FSharp/Configuration/ModuleConfigurationTests.fs new file mode 100644 index 0000000000..1b00b5eb05 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Configuration/ModuleConfigurationTests.fs @@ -0,0 +1,421 @@ +namespace ModularPipelines.UnitTests.FSharp.Configuration + +open System +open System.Threading.Tasks +open ModularPipelines.Configuration +open ModularPipelines.Context +open ModularPipelines.Models +open Moq +open Polly +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type ModuleConfigurationTests() = + [] + member _.Default_SkipCondition_IsNull() = async { + let config = ModuleConfiguration.Default + do! check(Assert.That(config.SkipCondition).IsNull()) + } + + [] + member _.Default_Timeout_IsNull() = async { + let config = ModuleConfiguration.Default + do! check(Assert.That(config.Timeout.HasValue).IsFalse()) + } + + [] + member _.Default_RetryPolicyFactory_IsNull() = async { + let config = ModuleConfiguration.Default + do! check(Assert.That(config.RetryPolicyFactory).IsNull()) + } + + [] + member _.Default_IgnoreFailuresCondition_IsNull() = async { + let config = ModuleConfiguration.Default + do! check(Assert.That(config.IgnoreFailuresCondition).IsNull()) + } + + [] + member _.Default_AlwaysRun_IsFalse() = async { + let config = ModuleConfiguration.Default + do! check(Assert.That(config.AlwaysRun).IsFalse()) + } + + [] + member _.Default_OnBeforeExecute_IsNull() = async { + let config = ModuleConfiguration.Default + do! check(Assert.That(config.OnBeforeExecute).IsNull()) + } + + [] + member _.Default_OnAfterExecute_IsNull() = async { + let config = ModuleConfiguration.Default + do! check(Assert.That(config.OnAfterExecute).IsNull()) + } + + [] + member _.Create_ReturnsBuilder() = async { + let builder = ModuleConfiguration.Create() + do! check(Assert.That(builder).IsNotNull()) + do! check(Assert.That(builder).IsTypeOf()) + } + + [] + member _.WithSkipWhen_SyncBool_SetsSkipCondition() = async { + let config = + ModuleConfiguration.Create() + .WithSkipWhen(Func(fun () -> true)) + .Build() + + do! check(Assert.That(config.SkipCondition).IsNotNull()) + + let context = Mock.Of() + let! decision = config.SkipCondition.Invoke(context) |> Async.AwaitTask + + do! check(Assert.That(decision.ShouldSkip).IsTrue()) + } + + [] + member _.WithSkipWhen_SyncBoolFalse_ReturnsDoNotSkip() = async { + let config = + ModuleConfiguration.Create() + .WithSkipWhen(Func(fun () -> false)) + .Build() + + let context = Mock.Of() + let! decision = config.SkipCondition.Invoke(context) |> Async.AwaitTask + + do! check(Assert.That(decision.ShouldSkip).IsFalse()) + } + + [] + member _.WithSkipWhen_AsyncBool_SetsSkipCondition() = async { + let config = + ModuleConfiguration.Create() + .WithSkipWhen( + Func>(fun () -> + task { + do! Task.Delay(1) + return true + }) + ) + .Build() + + do! check(Assert.That(config.SkipCondition).IsNotNull()) + + let context = Mock.Of() + let! decision = config.SkipCondition.Invoke(context) |> Async.AwaitTask + + do! check(Assert.That(decision.ShouldSkip).IsTrue()) + } + + [] + member _.WithSkipWhen_SyncSkipDecision_SetsSkipCondition() = async { + let expectedDecision = SkipDecision.Skip("Test reason") + + let config = + ModuleConfiguration.Create() + .WithSkipWhen(Func(fun () -> expectedDecision)) + .Build() + + let context = Mock.Of() + let! decision = config.SkipCondition.Invoke(context) |> Async.AwaitTask + + do! check(Assert.That(decision.ShouldSkip).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(decision.Reason), "Test reason")) + } + + [] + member _.WithSkipWhen_AsyncSkipDecision_SetsSkipCondition() = async { + let expectedDecision = SkipDecision.Skip("Async reason") + + let config = + ModuleConfiguration.Create() + .WithSkipWhen( + Func>(fun () -> + task { + do! Task.Delay(1) + return expectedDecision + }) + ) + .Build() + + let context = Mock.Of() + let! decision = config.SkipCondition.Invoke(context) |> Async.AwaitTask + + do! check(Assert.That(decision.ShouldSkip).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(decision.Reason), "Async reason")) + } + + [] + member _.WithSkipWhen_WithContext_SyncBool_SetsSkipCondition() = async { + let config = + ModuleConfiguration.Create() + .WithSkipWhen(Func(fun ctx -> not (obj.ReferenceEquals(ctx, null)))) + .Build() + + let context = Mock.Of() + let! decision = config.SkipCondition.Invoke(context) |> Async.AwaitTask + + do! check(Assert.That(decision.ShouldSkip).IsTrue()) + } + + [] + member _.WithSkipWhen_WithContext_AsyncBool_SetsSkipCondition() = async { + let config = + ModuleConfiguration.Create() + .WithSkipWhen( + Func>(fun ctx -> + task { + do! Task.Delay(1) + return not (obj.ReferenceEquals(ctx, null)) + }) + ) + .Build() + + let context = Mock.Of() + let! decision = config.SkipCondition.Invoke(context) |> Async.AwaitTask + + do! check(Assert.That(decision.ShouldSkip).IsTrue()) + } + + [] + member _.WithSkipWhen_WithContext_SyncSkipDecision_SetsSkipCondition() = async { + let config = + ModuleConfiguration.Create() + .WithSkipWhen( + Func(fun ctx -> + if not (obj.ReferenceEquals(ctx, null)) then + SkipDecision.Skip("Has context") + else + SkipDecision.DoNotSkip) + ) + .Build() + + let context = Mock.Of() + let! decision = config.SkipCondition.Invoke(context) |> Async.AwaitTask + + do! check(Assert.That(decision.ShouldSkip).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(decision.Reason), "Has context")) + } + + [] + member _.WithSkipWhen_WithContext_AsyncSkipDecision_SetsSkipCondition() = async { + let config = + ModuleConfiguration.Create() + .WithSkipWhen( + Func>(fun ctx -> + task { + do! Task.Delay(1) + + return + if not (obj.ReferenceEquals(ctx, null)) then + SkipDecision.Skip("Async context") + else + SkipDecision.DoNotSkip + }) + ) + .Build() + + let context = Mock.Of() + let! decision = config.SkipCondition.Invoke(context) |> Async.AwaitTask + + do! check(Assert.That(decision.ShouldSkip).IsTrue()) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(decision.Reason), "Async context")) + } + + [] + member _.WithTimeout_SetsTimeout() = async { + let timeout = TimeSpan.FromMinutes(5.0) + + let config = + ModuleConfiguration.Create() + .WithTimeout(timeout) + .Build() + + do! check(Assert.That(config.Timeout.HasValue).IsTrue()) + do! check(Assert.That(config.Timeout.Value = timeout).IsTrue()) + } + + [] + member _.WithRetryPolicy_Direct_SetsRetryPolicyFactory() = async { + let policy = Policy.NoOpAsync() + + let config = + ModuleConfiguration.Create() + .WithRetryPolicy(policy) + .Build() + + do! check(Assert.That(config.RetryPolicyFactory).IsNotNull()) + + let context = Mock.Of() + let result = config.RetryPolicyFactory.Invoke(context) + + do! check(Assert.That(obj.ReferenceEquals(result, policy)).IsTrue()) + } + + [] + member _.WithRetryPolicy_Factory_SetsRetryPolicyFactory() = async { + let policy = Policy.NoOpAsync() + + let config = + ModuleConfiguration.Create() + .WithRetryPolicy(Func(fun _ -> policy)) + .Build() + + do! check(Assert.That(config.RetryPolicyFactory).IsNotNull()) + + let context = Mock.Of() + let result = config.RetryPolicyFactory.Invoke(context) + + do! check(Assert.That(obj.ReferenceEquals(result, policy)).IsTrue()) + } + + [] + member _.WithRetryCount_SetsRetryPolicyFactory() = async { + let config = + ModuleConfiguration.Create() + .WithRetryCount(3) + .Build() + + do! check(Assert.That(config.RetryPolicyFactory).IsNotNull()) + } + + [] + member _.WithIgnoreFailures_Always_SetsIgnoreFailuresCondition() = async { + let config = + ModuleConfiguration.Create() + .WithIgnoreFailures() + .Build() + + do! check(Assert.That(config.IgnoreFailuresCondition).IsNotNull()) + + let context = Mock.Of() + let! result = config.IgnoreFailuresCondition.Invoke(context, Exception("test")) |> Async.AwaitTask + + do! check(Assert.That(result).IsTrue()) + } + + [] + member _.WithIgnoreFailuresWhen_SyncCondition_SetsIgnoreFailuresCondition() = async { + let config = + ModuleConfiguration.Create() + .WithIgnoreFailuresWhen(Func(fun _ ex -> ex.Message = "ignore")) + .Build() + + do! check(Assert.That(config.IgnoreFailuresCondition).IsNotNull()) + + let context = Mock.Of() + + let! shouldIgnore = config.IgnoreFailuresCondition.Invoke(context, Exception("ignore")) |> Async.AwaitTask + do! check(Assert.That(shouldIgnore).IsTrue()) + + let! shouldNotIgnore = config.IgnoreFailuresCondition.Invoke(context, Exception("fail")) |> Async.AwaitTask + do! check(Assert.That(shouldNotIgnore).IsFalse()) + } + + [] + member _.WithIgnoreFailuresWhen_AsyncCondition_SetsIgnoreFailuresCondition() = async { + let config = + ModuleConfiguration.Create() + .WithIgnoreFailuresWhen( + Func>(fun _ ex -> + task { + do! Task.Delay(1) + return ex.Message = "ignore" + }) + ) + .Build() + + let context = Mock.Of() + + let! shouldIgnore = config.IgnoreFailuresCondition.Invoke(context, Exception("ignore")) |> Async.AwaitTask + do! check(Assert.That(shouldIgnore).IsTrue()) + + let! shouldNotIgnore = config.IgnoreFailuresCondition.Invoke(context, Exception("fail")) |> Async.AwaitTask + do! check(Assert.That(shouldNotIgnore).IsFalse()) + } + + [] + member _.WithAlwaysRun_SetsAlwaysRun() = async { + let config = + ModuleConfiguration.Create() + .WithAlwaysRun() + .Build() + + do! check(Assert.That(config.AlwaysRun).IsTrue()) + } + + [] + member _.WithBeforeExecute_SetsOnBeforeExecute() = async { + let mutable executed = false + + let config = + ModuleConfiguration.Create() + .WithBeforeExecute( + Func(fun _ -> + task { + do! Task.Delay(1) + executed <- true + }) + ) + .Build() + + do! check(Assert.That(config.OnBeforeExecute).IsNotNull()) + + let context = Mock.Of() + do! config.OnBeforeExecute.Invoke(context) |> Async.AwaitTask + + do! check(Assert.That(executed).IsTrue()) + } + + [] + member _.WithAfterExecute_SetsOnAfterExecute() = async { + let mutable executed = false + + let config = + ModuleConfiguration.Create() + .WithAfterExecute( + Func(fun _ -> + task { + do! Task.Delay(1) + executed <- true + }) + ) + .Build() + + do! check(Assert.That(config.OnAfterExecute).IsNotNull()) + + let context = Mock.Of() + do! config.OnAfterExecute.Invoke(context) |> Async.AwaitTask + + do! check(Assert.That(executed).IsTrue()) + } + + [] + member _.Builder_FluentChaining_AllMethodsChain() = async { + let policy = Policy.NoOpAsync() + let timeout = TimeSpan.FromMinutes(1.0) + + let config = + ModuleConfiguration.Create() + .WithSkipWhen(Func(fun () -> false)) + .WithTimeout(timeout) + .WithRetryPolicy(policy) + .WithIgnoreFailures() + .WithAlwaysRun() + .WithBeforeExecute(Func(fun _ -> Task.CompletedTask)) + .WithAfterExecute(Func(fun _ -> Task.CompletedTask)) + .Build() + + do! check(Assert.That(config.SkipCondition).IsNotNull()) + do! check(Assert.That(config.Timeout.HasValue).IsTrue()) + do! check(Assert.That(config.Timeout.Value = timeout).IsTrue()) + do! check(Assert.That(config.RetryPolicyFactory).IsNotNull()) + do! check(Assert.That(config.IgnoreFailuresCondition).IsNotNull()) + do! check(Assert.That(config.AlwaysRun).IsTrue()) + do! check(Assert.That(config.OnBeforeExecute).IsNotNull()) + do! check(Assert.That(config.OnAfterExecute).IsNotNull()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Configuration/ModuleConfigureTests.fs b/test/ModularPipelines.UnitTests.FSharp/Configuration/ModuleConfigureTests.fs new file mode 100644 index 0000000000..3a99bd1797 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Configuration/ModuleConfigureTests.fs @@ -0,0 +1,60 @@ +namespace ModularPipelines.UnitTests.FSharp.Configuration + +open System +open System.Threading +open System.Threading.Tasks +open ModularPipelines.Configuration +open ModularPipelines.Context +open ModularPipelines.Modules +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type private TestModule() = + inherit Module() + + override _.ExecuteAsync(_, _) = + Task.FromResult("test") + +type private ConfiguredModule() = + inherit Module() + + override _.Configure() = + ModuleConfiguration.Create() + .WithTimeout(TimeSpan.FromSeconds(60.0)) + .WithAlwaysRun() + .Build() + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("test") + +type ModuleConfigureTests() = + [] + member _.Module_DefaultConfiguration_ReturnsDefault() = async { + let testModule = TestModule() + let config = (testModule :> IModule).Configuration + let isDefaultConfig = obj.ReferenceEquals(config, ModuleConfiguration.Default) + + do! check(Assert.That(isDefaultConfig).IsTrue()) + } + + [] + member _.Module_OverriddenConfigure_ReturnsCustomConfig() = async { + let configuredModule = ConfiguredModule() + let config = (configuredModule :> IModule).Configuration + + do! check(Assert.That(config.Timeout.HasValue).IsTrue()) + do! check(Assert.That(config.Timeout.Value = TimeSpan.FromSeconds(60.0)).IsTrue()) + do! check(Assert.That(config.AlwaysRun).IsTrue()) + } + + [] + member _.Module_Configuration_IsCached() = async { + let configuredModule = ConfiguredModule() + let config1 = (configuredModule :> IModule).Configuration + let config2 = (configuredModule :> IModule).Configuration + let isCachedReference = obj.ReferenceEquals(config1, config2) + + do! check(Assert.That(isCachedReference).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Console/OutputCoordinatorDeferredFlushTests.fs b/test/ModularPipelines.UnitTests.FSharp/Console/OutputCoordinatorDeferredFlushTests.fs new file mode 100644 index 0000000000..6ca9609737 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Console/OutputCoordinatorDeferredFlushTests.fs @@ -0,0 +1,72 @@ +namespace ModularPipelines.UnitTests.FSharp.Console + +open System +open System.Threading.Tasks +open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Logging +open ModularPipelines.Context +open ModularPipelines.Enums +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.Options +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type private Module1() = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, cancellationToken) = + task { + context.Logger.LogInformation("Module1 output") + do! Task.Delay(50, cancellationToken) + return true + } + +type private Module2() = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, _) = + task { + context.Logger.LogInformation("Module2 output") + do! Task.Yield() + return true + } + +type OutputCoordinatorDeferredFlushTests() = + [] + member _.Pipeline_Completes_When_Progress_Disabled() = async { + let! host = + TestPipelineHostBuilder.Create() + .ConfigureServices(Action(fun _ services -> + services.Configure(fun (options: PipelineOptions) -> + options.ShowProgressInConsole <- false) + |> ignore)) + .AddModule() + .BuildHostAsync() + |> Async.AwaitTask + + let! result = host.ExecutePipelineAsync() |> Async.AwaitTask + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + do! host.DisposeAsync().AsTask() |> Async.AwaitTask + } + + [] + member _.Pipeline_With_Multiple_Modules_Completes_Successfully() = async { + let! host = + TestPipelineHostBuilder.Create() + .ConfigureServices(Action(fun _ services -> + services.Configure(fun (options: PipelineOptions) -> + options.ShowProgressInConsole <- false) + |> ignore)) + .AddModule() + .AddModule() + .BuildHostAsync() + |> Async.AwaitTask + + let! result = host.ExecutePipelineAsync() |> Async.AwaitTask + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + do! host.DisposeAsync().AsTask() |> Async.AwaitTask + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Context/CommandLineBuilderTests.fs b/test/ModularPipelines.UnitTests.FSharp/Context/CommandLineBuilderTests.fs new file mode 100644 index 0000000000..1018327dbc --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Context/CommandLineBuilderTests.fs @@ -0,0 +1,136 @@ +namespace ModularPipelines.UnitTests.FSharp.Context + +open System +open System.Collections.ObjectModel +open ModularPipelines.Attributes +open ModularPipelines.Context +open ModularPipelines.Options +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +[] +[] +type private TestAttributeOptions() = + inherit CommandLineToolOptions() + + [] + member val Force = Nullable() with get, set + + [] + member val Output: string = null with get, set + + override this.``$``() = + let clonedOptions = TestAttributeOptions() + clonedOptions.Force <- this.Force + clonedOptions.Output <- this.Output + clonedOptions :> CommandLineToolOptions + +[] +[] +type private TestPositionalOptions() = + inherit CommandLineToolOptions() + + [] + member val FilePath: string = null with get, set + + [] + member val ConfigPath: string = null with get, set + + override this.``$``() = + let clonedOptions = TestPositionalOptions() + clonedOptions.FilePath <- this.FilePath + clonedOptions.ConfigPath <- this.ConfigPath + clonedOptions :> CommandLineToolOptions + +type CommandLineBuilderTests() = + inherit TestBase() + + [] + member this.Build_FromGenericOptions_ReturnsCorrectCommandLine() = async { + let! builder = this.GetService() |> Async.AwaitTask + let options = GenericCommandLineToolOptions("echo", Arguments = [| "hello"; "world" |]) + let result = builder.Build(options) + + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.Tool), "echo")) + do! check(Assert.That((result.Arguments |> Seq.toArray) = [| "hello"; "world" |]).IsTrue()) + } + + [] + member this.Build_FromGenericOptions_WithRunSettings_AddsDoubleDash() = async { + let! builder = this.GetService() |> Async.AwaitTask + + let options = + GenericCommandLineToolOptions( + "dotnet", + Arguments = [| "test" |], + RunSettings = [| "--filter"; "Category=Unit" |] + ) + + let result = builder.Build(options) + + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.Tool), "dotnet")) + do! check(Assert.That((result.Arguments |> Seq.toArray) = [| "test"; "--"; "--filter"; "Category=Unit" |]).IsTrue()) + } + + [] + member this.Build_FromAttributeBasedOptions_ResolvesToolAndSubcommands() = async { + let! builder = this.GetService() |> Async.AwaitTask + let options = TestAttributeOptions(Force = Nullable true, Output = "/path/to/output") + let result = builder.Build(options) + let arguments = result.Arguments |> Seq.toArray + + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.Tool), "mytool")) + do! check(Assert.That(arguments |> Array.contains "sub").IsTrue()) + do! check(Assert.That(arguments |> Array.contains "command").IsTrue()) + do! check(Assert.That(arguments |> Array.contains "--force").IsTrue()) + do! check(Assert.That(arguments |> Array.contains "--output").IsTrue()) + do! check(Assert.That(arguments |> Array.contains "/path/to/output").IsTrue()) + } + + [] + member this.Build_WithPositionalArguments_PlacesCorrectly() = async { + let! builder = this.GetService() |> Async.AwaitTask + let options = TestPositionalOptions(FilePath = "test.cs", ConfigPath = "config.json") + let result = builder.Build(options) + let arguments = result.Arguments |> Seq.toArray + let fileIndex = arguments |> Array.tryFindIndex ((=) "test.cs") + let configIndex = arguments |> Array.tryFindIndex ((=) "config.json") + + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.Tool), "processor")) + do! check(Assert.That(fileIndex.IsSome).IsTrue()) + do! check(Assert.That(configIndex.IsSome).IsTrue()) + do! check(Assert.That(fileIndex.Value < configIndex.Value).IsTrue()) + } + + [] + member this.Build_ReturnsImmutableCommandLine() = async { + let! builder = this.GetService() |> Async.AwaitTask + let options = GenericCommandLineToolOptions("echo", Arguments = [| "original" |]) + let result = builder.Build(options) + + do! check(Assert.That(result.Arguments :? ReadOnlyCollection).IsTrue()) + } + + [] + member this.Build_ToString_FormatsCorrectly() = async { + let! builder = this.GetService() |> Async.AwaitTask + let options = GenericCommandLineToolOptions("git", Arguments = [| "status"; "-s" |]) + let result = builder.Build(options) + + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.ToString()), "git status -s")) + } + + [] + member this.Build_SkipsDuplicateToolInArguments() = async { + let! builder = this.GetService() |> Async.AwaitTask + let options = GenericCommandLineToolOptions("git", Arguments = [| "git"; "status" |]) + let result = builder.Build(options) + let arguments = result.Arguments |> Seq.toArray + + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.Tool), "git")) + do! check(Assert.That(arguments |> Array.filter ((=) "git") |> Array.isEmpty).IsTrue()) + do! check(Assert.That(arguments |> Array.contains "status").IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Context/ContextExtensionsTests.fs b/test/ModularPipelines.UnitTests.FSharp/Context/ContextExtensionsTests.fs new file mode 100644 index 0000000000..6a1128d275 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Context/ContextExtensionsTests.fs @@ -0,0 +1,122 @@ +namespace ModularPipelines.UnitTests.FSharp.Context + +open System +open Microsoft.Extensions.Configuration +open ModularPipelines.Context +open ModularPipelines.Context.Domains +open ModularPipelines.Extensions +open Moq +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type private TestService() = class end + +type ContextExtensionsTests() = + [] + member _.GetService_ShouldResolveFromDI() = async { + let mockServices = Mock() + let expectedService = TestService() + mockServices.Setup(fun services -> services.Get()).Returns(expectedService) |> ignore + + let mockContext = Mock() + mockContext.Setup(fun context -> context.Services).Returns(mockServices.Object) |> ignore + + let result = mockContext.Object.GetService() + do! check(Assert.That(Object.ReferenceEquals(result, expectedService)).IsTrue()) + } + + [] + member _.GetService_WhenServiceNotRegistered_ShouldThrow() = async { + let mockServices = Mock() + mockServices.Setup(fun services -> services.Get()).Returns(Unchecked.defaultof) |> ignore + + let mockContext = Mock() + mockContext.Setup(fun context -> context.Services).Returns(mockServices.Object) |> ignore + + let mutable threw = false + + try + mockContext.Object.GetService() |> ignore + with :? InvalidOperationException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.TryGetService_ShouldReturnServiceOrNull() = async { + let mockServices = Mock() + mockServices.Setup(fun services -> services.Get()).Returns(Unchecked.defaultof) |> ignore + + let mockContext = Mock() + mockContext.Setup(fun context -> context.Services).Returns(mockServices.Object) |> ignore + + let result = mockContext.Object.TryGetService() + do! check(Assert.That(result).IsNull()) + } + + [] + member _.TryGetService_WhenServiceExists_ShouldReturnService() = async { + let mockServices = Mock() + let expectedService = TestService() + mockServices.Setup(fun services -> services.Get()).Returns(expectedService) |> ignore + + let mockContext = Mock() + mockContext.Setup(fun context -> context.Services).Returns(mockServices.Object) |> ignore + + let result = mockContext.Object.TryGetService() + do! check(Assert.That(Object.ReferenceEquals(result, expectedService)).IsTrue()) + } + + [] + member _.GetConfigValue_ShouldReturnConfigurationValue() = async { + let mockConfiguration = Mock() + mockConfiguration.Setup(fun configuration -> configuration.["TestKey"]).Returns("TestValue") |> ignore + + let mockServices = Mock() + mockServices.Setup(fun services -> services.Configuration).Returns(mockConfiguration.Object) |> ignore + + let mockContext = Mock() + mockContext.Setup(fun context -> context.Services).Returns(mockServices.Object) |> ignore + + let result = mockContext.Object.GetConfigValue("TestKey") + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result), "TestValue")) + } + + [] + member _.GetRequiredConfigValue_WhenValueExists_ShouldReturnValue() = async { + let mockConfiguration = Mock() + mockConfiguration.Setup(fun configuration -> configuration.["TestKey"]).Returns("TestValue") |> ignore + + let mockServices = Mock() + mockServices.Setup(fun services -> services.Configuration).Returns(mockConfiguration.Object) |> ignore + + let mockContext = Mock() + mockContext.Setup(fun context -> context.Services).Returns(mockServices.Object) |> ignore + + let result = mockContext.Object.GetRequiredConfigValue("TestKey") + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result), "TestValue")) + } + + [] + member _.GetRequiredConfigValue_WhenValueMissing_ShouldThrow() = async { + let mockConfiguration = Mock() + mockConfiguration.Setup(fun configuration -> configuration.["MissingKey"]).Returns(Unchecked.defaultof) |> ignore + + let mockServices = Mock() + mockServices.Setup(fun services -> services.Configuration).Returns(mockConfiguration.Object) |> ignore + + let mockContext = Mock() + mockContext.Setup(fun context -> context.Services).Returns(mockServices.Object) |> ignore + + let mutable threw = false + + try + mockContext.Object.GetRequiredConfigValue("MissingKey") |> ignore + with :? InvalidOperationException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Context/ContextHierarchyTests.fs b/test/ModularPipelines.UnitTests.FSharp/Context/ContextHierarchyTests.fs new file mode 100644 index 0000000000..b0d148bd13 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Context/ContextHierarchyTests.fs @@ -0,0 +1,60 @@ +namespace ModularPipelines.UnitTests.FSharp.Context + +open ModularPipelines.Context +open ModularPipelines.Context.Domains +open ModularPipelines.Logging +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type ContextHierarchyTests() = + [] + member _.IModuleContext_ShouldInheritFromIPipelineContext() = async { + let interfaces = typeof.GetInterfaces() + do! check(Assert.That(interfaces |> Array.contains typeof).IsTrue()) + } + + [] + member _.IPipelineHookContext_ShouldInheritFromIPipelineContext() = async { + let interfaces = typeof.GetInterfaces() + do! check(Assert.That(interfaces |> Array.contains typeof).IsTrue()) + } + + [] + member _.IPipelineContext_ShouldHaveExpectedDomainProperties() = async { + let contextType = typeof + let assertProperty name expectedType = async { + let propertyInfo = contextType.GetProperty(name) + do! check(Assert.That(propertyInfo).IsNotNull()) + do! check(Assert.That(propertyInfo.PropertyType = expectedType).IsTrue()) + } + + do! assertProperty "Logger" typeof + do! assertProperty "Shell" typeof + do! assertProperty "Files" typeof + do! assertProperty "Data" typeof + do! assertProperty "Environment" typeof + do! assertProperty "Installers" typeof + do! assertProperty "Network" typeof + do! assertProperty "Security" typeof + do! assertProperty "Services" typeof + } + + [] + member _.IModuleContext_ShouldHaveGetModuleMethods() = async { + let moduleContextType = typeof + let getModuleMethods = moduleContextType.GetMethods() |> Array.filter (fun methodInfo -> methodInfo.Name = "GetModule") + let getModuleIfRegisteredMethods = moduleContextType.GetMethods() |> Array.filter (fun methodInfo -> methodInfo.Name = "GetModuleIfRegistered") + + do! check(Assert.That(getModuleMethods.Length >= 1).IsTrue()) + do! check(Assert.That(getModuleIfRegisteredMethods.Length >= 1).IsTrue()) + } + + [] + member _.IModuleContext_ShouldHaveSubModuleMethods() = async { + let moduleContextType = typeof + let subModuleMethods = moduleContextType.GetMethods() |> Array.filter (fun methodInfo -> methodInfo.Name = "SubModule") + + do! check(Assert.That(subModuleMethods.Length >= 1).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Context/GitInformationTests.fs b/test/ModularPipelines.UnitTests.FSharp/Context/GitInformationTests.fs new file mode 100644 index 0000000000..54c1c0775f --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Context/GitInformationTests.fs @@ -0,0 +1,22 @@ +namespace ModularPipelines.UnitTests.FSharp.Context + +open System +open ModularPipelines.Context +open ModularPipelines.Git.Extensions +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type GitInformationTests() = + inherit TestBase() + + [] + member this.Can_Send_Request_With_String_To_Request_Implicit_Conversion() = async { + let! context = this.GetService() |> Async.AwaitTask + let branch = context.Git().Information.BranchName + + do! check(Assert.That(branch).IsNotNull()) + do! check(Assert.That(not (String.IsNullOrWhiteSpace(branch))).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Context/HttpTests.fs b/test/ModularPipelines.UnitTests.FSharp/Context/HttpTests.fs new file mode 100644 index 0000000000..129eb711a8 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Context/HttpTests.fs @@ -0,0 +1,154 @@ +namespace ModularPipelines.UnitTests.FSharp.Context + +open System +open System.IO +open System.Net.Http +open System.Threading.Tasks +open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Logging +open ModularPipelines.Http +open ModularPipelines.Options +open ModularPipelines.TestHelpers +open NReco.Logging.File +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open Vertical.SpectreLogger.Options + +type HttpTests() = + inherit TestBase() + + member private this.GetHttpWithFileLogger(file: string) : Task = + this.GetService( + Action(fun services -> + services.AddLogging(fun builder -> + services.Configure(fun (options: SpectreLoggerOptions) -> + options.MinimumLogLevel <- LogLevel.Information) + |> ignore + + services.Configure(fun (options: LoggerFilterOptions) -> + options.MinLevel <- LogLevel.Information) + |> ignore + + builder.AddFile(file) |> ignore) + |> ignore) + ) + + [] + member this.Can_Send_Request_With_String_To_Request_Implicit_Conversion() = async { + let! http = this.GetService() |> Async.AwaitTask + use! response = http.SendAsync(Uri("https://thomhurst.github.io/TUnit")) |> Async.AwaitTask + do! check(Assert.That(response.IsSuccessStatusCode).IsTrue()) + } + + [] + member this.When_Log_Request_False_Then_Do_Not_Log_Request() = async { + let file = Path.Combine(TestContext.WorkingDirectory, Guid.NewGuid().ToString("N") + ".txt") + + try + let! struct (http, host) = this.GetHttpWithFileLogger(file) |> Async.AwaitTask + + do! + http.SendAsync( + HttpOptions( + HttpRequestMessage(HttpMethod.Get, Uri("https://thomhurst.github.io/TUnit")), + ThrowOnNonSuccessStatusCode = false, + LoggingType = HttpLoggingType.Response + ) + ) + |> Async.AwaitTask + |> Async.Ignore + + do! host.DisposeAsync().AsTask() |> Async.AwaitTask + + let! logFile = File.ReadAllTextAsync(file) |> Async.AwaitTask + do! check(Assert.That(not (logFile.Contains("HTTP Request:"))).IsTrue()) + do! check(Assert.That(not (logFile.Contains("GET https://thomhurst.github.io/TUnit HTTP/1.1"))).IsTrue()) + do! check(Assert.That(logFile.Contains("HTTP Response:")).IsTrue()) + do! check(Assert.That(logFile.Contains("Server: GitHub.com")).IsTrue()) + finally + if File.Exists(file) then + File.Delete(file) + } + + [] + member this.When_Log_Response_False_Then_Do_Not_Log_Response() = async { + let file = Path.Combine(TestContext.WorkingDirectory, Guid.NewGuid().ToString("N") + ".txt") + + try + let! struct (http, host) = this.GetHttpWithFileLogger(file) |> Async.AwaitTask + + do! + http.SendAsync( + HttpOptions( + HttpRequestMessage(HttpMethod.Get, Uri("https://thomhurst.github.io/TUnit")), + ThrowOnNonSuccessStatusCode = false, + LoggingType = HttpLoggingType.Request + ) + ) + |> Async.AwaitTask + |> Async.Ignore + + do! host.DisposeAsync().AsTask() |> Async.AwaitTask + + let! logFile = File.ReadAllTextAsync(file) |> Async.AwaitTask + do! check(Assert.That(logFile.Contains("HTTP Request:")).IsTrue()) + do! check(Assert.That(logFile.Contains("GET https://thomhurst.github.io/TUnit HTTP/1.1")).IsTrue()) + do! check(Assert.That(not (logFile.Contains("HTTP Response:"))).IsTrue()) + do! check(Assert.That(not (logFile.Contains("Server: GitHub.com"))).IsTrue()) + finally + if File.Exists(file) then + File.Delete(file) + } + + [] + [] + [] + member this.Assert_LoggingHttpClient_Logs_As_Expected(customHttpClient: bool) = async { + let file = Path.Combine(TestContext.WorkingDirectory, Guid.NewGuid().ToString("N") + ".txt") + + try + let! struct (http, host) = this.GetHttpWithFileLogger(file) |> Async.AwaitTask + + if customHttpClient then + use customClient = new HttpClient() + + do! + http.SendAsync( + HttpOptions( + HttpRequestMessage(HttpMethod.Get, Uri("https://thomhurst.github.io/TUnit")), + ThrowOnNonSuccessStatusCode = false, + HttpClient = customClient + ) + ) + |> Async.AwaitTask + |> Async.Ignore + else + use! response = http.GetLoggingHttpClient().GetAsync(Uri("https://thomhurst.github.io/TUnit")) |> Async.AwaitTask + () + + do! host.DisposeAsync().AsTask() |> Async.AwaitTask + + let! logFile = File.ReadAllTextAsync(file) |> Async.AwaitTask + let! logFileLines = File.ReadAllLinesAsync(file) |> Async.AwaitTask + let indexOfRequest = logFileLines |> Array.findIndex (fun line -> line.Contains("HTTP Request:")) + let indexOfStatusCode = logFileLines |> Array.findIndex (fun line -> line.Contains("HTTP Status:")) + let indexOfDuration = logFileLines |> Array.findIndex (fun line -> line.Contains("Duration:")) + let indexOfResponse = logFileLines |> Array.findIndex (fun line -> line.Contains("HTTP Response:")) + + do! check(Assert.That(logFile.Contains("HTTP Request:")).IsTrue()) + do! check(Assert.That(logFile.Contains("GET https://thomhurst.github.io/TUnit HTTP/1.1")).IsTrue()) + do! check(Assert.That(logFile.Contains("HTTP Response:")).IsTrue()) + do! check(Assert.That(logFile.Contains("Headers")).IsTrue()) + do! check(Assert.That(logFile.Contains("Server: GitHub.com")).IsTrue()) + do! check(Assert.That(logFile.Contains("Body")).IsTrue()) + do! check(Assert.That(logFile.Contains("Duration:")).IsTrue()) + do! check(Assert.That(logFile.Contains("HTTP Status:")).IsTrue()) + do! check(Assert.That(indexOfRequest < indexOfStatusCode).IsTrue()) + do! check(Assert.That(indexOfStatusCode < indexOfDuration).IsTrue()) + do! check(Assert.That(indexOfDuration < indexOfResponse).IsTrue()) + finally + if File.Exists(file) then + File.Delete(file) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Context/InterfaceVisibilityTests.fs b/test/ModularPipelines.UnitTests.FSharp/Context/InterfaceVisibilityTests.fs new file mode 100644 index 0000000000..43f927397f --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Context/InterfaceVisibilityTests.fs @@ -0,0 +1,74 @@ +namespace ModularPipelines.UnitTests.FSharp.Context + +open ModularPipelines.Context +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type InterfaceVisibilityTests() = + [] + member _.EngineInterfaces_ShouldBeInternal() = async { + let assembly = typeof.Assembly + + let engineInterfaces = + assembly.GetTypes() + |> Array.filter (fun t -> t.IsInterface) + |> Array.filter (fun t -> t.Namespace <> null && t.Namespace.Contains("Engine")) + |> Array.filter (fun t -> t.Name.StartsWith("IPipeline")) + + for iface in engineInterfaces do + do! check(Assert.That(iface.IsPublic).IsFalse()) + } + + [] + member _.UserFacingContextInterfaces_ShouldBePublic() = async { + let assembly = typeof.Assembly + + let expectedPublicInterfaces = + [| + "ModularPipelines.Context", "IPipelineContext" + "ModularPipelines.Context", "IPipelineHookContext" + "ModularPipelines.Context", "IModuleContext" + "ModularPipelines.Context.Domains", "IShellContext" + "ModularPipelines.Context.Domains", "IFilesContext" + "ModularPipelines.Context.Domains", "IDataContext" + "ModularPipelines.Context.Domains", "IEnvironmentDomainContext" + "ModularPipelines.Context.Domains", "IInstallersContext" + "ModularPipelines.Context.Domains", "INetworkContext" + "ModularPipelines.Context.Domains", "ISecurityContext" + "ModularPipelines.Context.Domains", "IServicesContext" + |] + + for ns, interfaceName in expectedPublicInterfaces do + let iface = assembly.GetType($"{ns}.{interfaceName}") + do! check(Assert.That(iface).IsNotNull()) + do! check(Assert.That(iface.IsPublic).IsTrue()) + } + + [] + member _.ExtensionPointInterfaces_ShouldBePublic() = async { + let assembly = typeof.Assembly + + let extensionPointInterfaces = + [| + "ModularPipelines", "IPipeline" + "ModularPipelines.Interfaces", "IPipelineGlobalHooks" + "ModularPipelines.Interfaces", "IPipelineModuleHooks" + "ModularPipelines.Requirements", "IPipelineRequirement" + |] + + for ns, name in extensionPointInterfaces do + let iface = assembly.GetType($"{ns}.{name}") + do! check(Assert.That(iface).IsNotNull()) + do! check(Assert.That(iface.IsPublic).IsTrue()) + } + + [] + member _.IPipelineServiceContainerWrapper_ShouldBeInternal() = async { + let assembly = typeof.Assembly + let iface = assembly.GetType("ModularPipelines.DependencyInjection.IPipelineServiceContainerWrapper") + + do! check(Assert.That(iface).IsNotNull()) + do! check(Assert.That(not iface.IsPublic).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/CategoryFilterDependencyTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/CategoryFilterDependencyTests.fs new file mode 100644 index 0000000000..6776976c3c --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/CategoryFilterDependencyTests.fs @@ -0,0 +1,116 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System.Threading.Tasks +open ModularPipelines.Attributes +open ModularPipelines.Context +open ModularPipelines.Enums +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +[] +type CompileModule() = + inherit SimpleTestModule() + override _.Result = "compiled" + +[] +[, Optional = true)>] +type TestModuleWithOptionalDep() = + inherit Module() + + override _.ExecuteAsync(context, _) = + task { + let compile = context.GetModuleIfRegistered() + + if isNull compile then + return "test-without-compile" + else + let! result = compile.CompletionSource.Task + return if result.IsSkipped then "test-compile-skipped" else $"test-with-{result.ValueOrDefault}" + } + +[] +[, Optional = true)>] +type TestModuleWithOptionalDepForCategoryFilter() = + inherit Module() + + override _.ExecuteAsync(context, _) = + task { + let compile = context.GetModuleIfRegistered() + + if isNull compile then + return "test-without-compile" + else + let! result = compile.CompletionSource.Task + return if result.IsSkipped then "test-compile-skipped" else $"test-with-{result.ValueOrDefault}" + } + +type CategoryFilterDependencyTests() = + inherit TestBase() + + [] + member _.Optional_Dependency_Works_When_Filtered_By_Category() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ConfigurePipelineOptions(fun opt -> opt.RunOnlyCategories <- ResizeArray([ "test" ])) + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + + let testModule = + pipelineSummary.Modules + |> Seq.find (fun moduleInstance -> moduleInstance :? TestModuleWithOptionalDep) + :?> TestModuleWithOptionalDep + + let! result = testModule.CompletionSource.Task |> Async.AwaitTask + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.ValueOrDefault), "test-compile-skipped")) + } + + [] + member _.Optional_Dependency_Is_Skipped_When_Filtered_By_Category() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ConfigurePipelineOptions(fun opt -> opt.RunOnlyCategories <- ResizeArray([ "test" ])) + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + + let testModule = + pipelineSummary.Modules + |> Seq.find (fun moduleInstance -> moduleInstance :? TestModuleWithOptionalDepForCategoryFilter) + :?> TestModuleWithOptionalDepForCategoryFilter + + let! result = testModule.CompletionSource.Task |> Async.AwaitTask + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.ValueOrDefault), "test-compile-skipped")) + } + + [] + member _.Both_Categories_Run_Successfully() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ConfigurePipelineOptions(fun opt -> opt.RunOnlyCategories <- ResizeArray([ "compile"; "test" ])) + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + + let testModule = + pipelineSummary.Modules + |> Seq.find (fun moduleInstance -> moduleInstance :? TestModuleWithOptionalDep) + :?> TestModuleWithOptionalDep + + let! result = testModule.CompletionSource.Task |> Async.AwaitTask + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.ValueOrDefault), "test-with-compiled")) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/CircularDependencyDetectionTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/CircularDependencyDetectionTests.fs new file mode 100644 index 0000000000..bd1330affa --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/CircularDependencyDetectionTests.fs @@ -0,0 +1,203 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System +open System.Collections.Generic +open System.Threading.Tasks +open ModularPipelines.Attributes +open ModularPipelines.Context +open ModularPipelines.Engine +open ModularPipelines.Exceptions +open ModularPipelines.Modules +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +[)>] +type DirectCycleModuleA() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) +and [)>] DirectCycleModuleB() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + +[)>] +type TripleCycleModuleA() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) +and [)>] TripleCycleModuleB() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) +and [)>] TripleCycleModuleC() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + +[)>] +type LinearModuleA() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) +and [)>] LinearModuleB() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) +and LinearModuleC() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + +type IndependentModuleA() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + +type IndependentModuleB() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + +type ComplexGraphRoot() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + +[)>] +[)>] +type ComplexGraphCycleA() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) +and [)>] ComplexGraphCycleB() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + +type CircularDependencyDetectionTests() = + [] + member _.ValidateNoCycles_WithDirectCycle_ThrowsCircularDependencyException() = async { + let mutable threw = false + + try + DependencyGraphValidator.ValidateNoCycles([| typeof; typeof |]) + with :? CircularDependencyException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.ValidateNoCycles_WithTripleCycle_ThrowsCircularDependencyException() = async { + let mutable threw = false + + try + DependencyGraphValidator.ValidateNoCycles([| typeof; typeof; typeof |]) + with :? CircularDependencyException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.ValidateNoCycles_WithLinearChain_DoesNotThrow() = async { + let mutable threw = false + + try + DependencyGraphValidator.ValidateNoCycles([| typeof; typeof; typeof |]) + with _ -> + threw <- true + + do! check(Assert.That(threw).IsFalse()) + } + + [] + member _.ValidateNoCycles_WithIndependentModules_DoesNotThrow() = async { + let mutable threw = false + + try + DependencyGraphValidator.ValidateNoCycles([| typeof; typeof |]) + with _ -> + threw <- true + + do! check(Assert.That(threw).IsFalse()) + } + + [] + member _.ValidateNoCycles_WithEmptyCollection_DoesNotThrow() = async { + let mutable threw = false + + try + DependencyGraphValidator.ValidateNoCycles(Array.empty) + with _ -> + threw <- true + + do! check(Assert.That(threw).IsFalse()) + } + + [] + member _.ValidateNoCycles_WithComplexGraphContainingCycle_ThrowsCircularDependencyException() = async { + let mutable threw = false + + try + DependencyGraphValidator.ValidateNoCycles([| typeof; typeof; typeof |]) + with :? CircularDependencyException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.ValidateNoCycles_ExceptionContainsCycleTypes() = async { + let mutable cycleTypes: IReadOnlyList = null + + try + DependencyGraphValidator.ValidateNoCycles([| typeof; typeof |]) + with :? CircularDependencyException as ex -> + cycleTypes <- ex.CycleTypes + + do! check(Assert.That(obj.ReferenceEquals(cycleTypes, null) = false).IsTrue()) + do! check(Assert.That(cycleTypes.Count >= 2).IsTrue()) + } + + [] + member _.ValidateNoCycles_ExceptionMessageShowsCyclePath() = async { + let mutable message = null + + try + DependencyGraphValidator.ValidateNoCycles([| typeof; typeof |]) + with :? CircularDependencyException as ex -> + message <- ex.Message + + do! check(Assert.That(message).IsNotNull()) + do! check(Assert.That(message.Contains("->")).IsTrue()) + do! check(Assert.That(message.Contains("Circular dependency detected")).IsTrue()) + } + + [] + member _.AddModulesFromAssembly_WithCircularDependency_ThrowsAtRegistrationTime() = async { + let mutable message = null + + try + DependencyGraphValidator.ValidateNoCycles([| typeof; typeof |]) + with :? CircularDependencyException as ex -> + message <- ex.Message + + do! check(Assert.That(message).IsNotNull()) + do! check(Assert.That(message.Contains("Circular dependency detected at registration")).IsTrue()) + } + + [] + member _.CreateWithCyclePath_FormatsMessageCorrectly() = async { + let cyclePath = List([ typeof; typeof; typeof ]) + let cycleException = CircularDependencyException.CreateWithCyclePath(cyclePath) + + do! check(Assert.That(cycleException.Message.Contains("**DirectCycleModuleA**")).IsTrue()) + do! check(Assert.That(cycleException.Message.Contains("->")).IsTrue()) + do! check(Assert.That(Seq.toList cycleException.CycleTypes = Seq.toList cyclePath).IsTrue()) + } + + [] + member _.CreateWithCyclePath_HighlightsStartAndEndOfCycle() = async { + let cyclePath = + List([ + typeof + typeof + typeof + typeof + ]) + + let cycleException = CircularDependencyException.CreateWithCyclePath(cyclePath) + + do! check(Assert.That(cycleException.Message.Contains("**TripleCycleModuleA**")).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnAllInheritingFromTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnAllInheritingFromTests.fs new file mode 100644 index 0000000000..1d28ff075a --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnAllInheritingFromTests.fs @@ -0,0 +1,154 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System +open System.Threading.Tasks +open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Time.Testing +open ModularPipelines.Attributes +open ModularPipelines.Context +open ModularPipelines.Engine +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +[] +module private DependsOnAllInheritingFromTestsHelpers = + [] + let toleranceMilliseconds = -25.0 + + let moduleDelay = TimeSpan.FromMilliseconds(50.0) + + +[] +type InheritingBaseModule() = + inherit Module() + +[] +type InheritingGenericBaseModule<'T>() = + inherit Module<'T>() + +type InheritingGenericModule1() = + inherit InheritingGenericBaseModule() + + override _.ExecuteAsync(_, cancellationToken) = + task { + do! Task.Delay(moduleDelay, cancellationToken) + return 42 + } + +type InheritingGenericModule2() = + inherit InheritingGenericBaseModule() + + override _.ExecuteAsync(_, cancellationToken) = + task { + do! Task.Delay(moduleDelay, cancellationToken) + return "test" + } + +[>)>] +type InheritingDependsOnOpenGenericModule() = + inherit Module() + + override _.ExecuteAsync(_, _) = + task { + do! Task.Yield() + return true + } + +type InheritingModule1() = + inherit InheritingBaseModule() + + override _.ExecuteAsync(_, cancellationToken) = + task { + do! Task.Delay(moduleDelay, cancellationToken) + return true + } + +[)>] +type InheritingModule2() = + inherit InheritingBaseModule() + + override _.ExecuteAsync(_, cancellationToken) = + task { + do! Task.Delay(moduleDelay, cancellationToken) + return true + } + +[, Optional = true)>] +type InheritingModule3() = + inherit InheritingBaseModule() + + override _.ExecuteAsync(_, cancellationToken) = + task { + do! Task.Delay(moduleDelay, cancellationToken) + return true + } + +[)>] +type InheritingModule4() = + inherit Module() + + override _.ExecuteAsync(_, _) = + task { + do! Task.Yield() + return true + } + +type DependsOnAllInheritingFromTests() = + inherit TestBase() + + [] + member _.No_Exception_Thrown_When_Dependent_Module_Present() = async { + let timeProvider = FakeTimeProvider() + + let! host = + TestPipelineHostBuilder.Create(TestHostSettings(), timeProvider) + .AddModule() + .AddModule() + .AddModule() + .AddModule() + .BuildHostAsync() + |> Async.AwaitTask + + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + + let resultRegistry = host.RootServices.GetRequiredService() + let result1 = resultRegistry.GetResult(typeof) + let result2 = resultRegistry.GetResult(typeof) + let result3 = resultRegistry.GetResult(typeof) + let result4 = resultRegistry.GetResult(typeof) + + do! check(Assert.That(result4.ModuleStart >= result1.ModuleStart.Add(moduleDelay.Add(TimeSpan.FromMilliseconds(toleranceMilliseconds)))).IsTrue()) + do! check(Assert.That(result4.ModuleStart >= result1.ModuleEnd).IsTrue()) + do! check(Assert.That(result4.ModuleStart >= result2.ModuleStart.Add(moduleDelay.Add(TimeSpan.FromMilliseconds(toleranceMilliseconds)))).IsTrue()) + do! check(Assert.That(result4.ModuleStart >= result2.ModuleEnd).IsTrue()) + do! check(Assert.That(result4.ModuleStart >= result3.ModuleStart.Add(moduleDelay.Add(TimeSpan.FromMilliseconds(toleranceMilliseconds)))).IsTrue()) + do! check(Assert.That(result4.ModuleStart >= result3.ModuleEnd).IsTrue()) + } + + [] + member _.DependsOnAllModulesInheritingFrom_Works_With_Open_Generic_Types() = async { + let timeProvider = FakeTimeProvider() + + let! host = + TestPipelineHostBuilder.Create(TestHostSettings(), timeProvider) + .AddModule() + .AddModule() + .AddModule() + .BuildHostAsync() + |> Async.AwaitTask + + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + + let resultRegistry = host.RootServices.GetRequiredService() + let result1 = resultRegistry.GetResult(typeof) + let result2 = resultRegistry.GetResult(typeof) + let dependentResult = resultRegistry.GetResult(typeof) + + do! check(Assert.That(dependentResult.ModuleStart >= result1.ModuleEnd).IsTrue()) + do! check(Assert.That(dependentResult.ModuleStart >= result2.ModuleEnd).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnModulesInCategoryAttributeTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnModulesInCategoryAttributeTests.fs new file mode 100644 index 0000000000..fb0d3c50f6 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnModulesInCategoryAttributeTests.fs @@ -0,0 +1,95 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System +open ModularPipelines.Attributes +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type CategoryAttributeTestModule() = + class + end + +type DependsOnModulesInCategoryAttributeTests() = + [] + member _.ShouldDependOn_ModuleInCategory_ReturnsTrue() = async { + let attr = DependsOnModulesInCategoryAttribute("infrastructure") + let context = MockDependencyContext().WithCategory(typeof, "infrastructure") + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsTrue()) + } + + [] + member _.ShouldDependOn_ModuleInDifferentCategory_ReturnsFalse() = async { + let attr = DependsOnModulesInCategoryAttribute("infrastructure") + let context = MockDependencyContext().WithCategory(typeof, "build") + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsFalse()) + } + + [] + member _.ShouldDependOn_CaseInsensitive_ReturnsTrue() = async { + let attr = DependsOnModulesInCategoryAttribute("INFRASTRUCTURE") + let context = MockDependencyContext().WithCategory(typeof, "infrastructure") + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsTrue()) + } + + [] + member _.ShouldDependOn_ModuleHasNoCategory_ReturnsFalse() = async { + let attr = DependsOnModulesInCategoryAttribute("infrastructure") + let context = MockDependencyContext() + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsFalse()) + } + + [] + member _.ShouldDependOn_CategoryMatchesExactly_ReturnsTrue() = async { + let attr = DependsOnModulesInCategoryAttribute("build-pipeline") + let context = MockDependencyContext().WithCategory(typeof, "build-pipeline") + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsTrue()) + } + + [] + member _.Constructor_WithNullCategory_ThrowsArgumentException() = async { + let mutable threw = false + + try + DependsOnModulesInCategoryAttribute(null) |> ignore + with :? ArgumentException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.Constructor_WithEmptyCategory_ThrowsArgumentException() = async { + let mutable threw = false + + try + DependsOnModulesInCategoryAttribute(String.Empty) |> ignore + with :? ArgumentException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.Constructor_WithWhitespaceCategory_ThrowsArgumentException() = async { + let mutable threw = false + + try + DependsOnModulesInCategoryAttribute(" ") |> ignore + with :? ArgumentException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.Category_Property_ReturnsConstructorValue() = async { + let attr = DependsOnModulesInCategoryAttribute("my-category") + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(attr.Category), "my-category")) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnModulesWithAttributeAttributeTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnModulesWithAttributeAttributeTests.fs new file mode 100644 index 0000000000..b3299c518e --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnModulesWithAttributeAttributeTests.fs @@ -0,0 +1,110 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System +open ModularPipelines.Attributes +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +[] +type CriticalAttribute() = + inherit Attribute() + +[] +type OtherAttribute() = + inherit Attribute() + +[] +type InheritableAttribute() = + inherit Attribute() + +[] +type CriticalModule() = + class + end + +type NonCriticalModule() = + class + end + +[] +type BaseModuleWithInheritableAttribute() = + class + end + +type DerivedModuleWithInheritedAttribute() = + inherit BaseModuleWithInheritableAttribute() + +[] +type ModuleWithDifferentAttribute() = + class + end + +[] +[] +type ModuleWithMultipleAttributes() = + class + end + +[] +type SerializableModule() = + class + end + +type DependsOnModulesWithAttributeAttributeTests() = + [] + member _.ShouldDependOn_ModuleHasAttribute_ReturnsTrue() = async { + let attr = DependsOnModulesWithAttributeAttribute() + let context = MockDependencyContext() + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsTrue()) + } + + [] + member _.ShouldDependOn_ModuleLacksAttribute_ReturnsFalse() = async { + let attr = DependsOnModulesWithAttributeAttribute() + let context = MockDependencyContext() + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsFalse()) + } + + [] + member _.ShouldDependOn_ModuleHasInheritedAttribute_ReturnsTrue() = async { + let attr = DependsOnModulesWithAttributeAttribute() + let context = MockDependencyContext() + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsTrue()) + } + + [] + member _.ShouldDependOn_ModuleHasDifferentAttribute_ReturnsFalse() = async { + let attr = DependsOnModulesWithAttributeAttribute() + let context = MockDependencyContext() + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsFalse()) + } + + [] + member _.ShouldDependOn_ModuleHasMultipleAttributesIncludingMatch_ReturnsTrue() = async { + let attr = DependsOnModulesWithAttributeAttribute() + let context = MockDependencyContext() + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsTrue()) + } + + [] + member _.ShouldDependOn_CheckingForSerializableAttribute_ReturnsTrue() = async { + let attr = DependsOnModulesWithAttributeAttribute() + let context = MockDependencyContext() + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsTrue()) + } + + [] + member _.ShouldDependOn_CheckingForSerializableAttribute_ReturnsFalse() = async { + let attr = DependsOnModulesWithAttributeAttribute() + let context = MockDependencyContext() + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsFalse()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnModulesWithTagAttributeTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnModulesWithTagAttributeTests.fs new file mode 100644 index 0000000000..e7a8f8d755 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnModulesWithTagAttributeTests.fs @@ -0,0 +1,95 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System +open ModularPipelines.Attributes +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type TagAttributeTestModule() = + class + end + +type DependsOnModulesWithTagAttributeTests() = + [] + member _.ShouldDependOn_ModuleHasTag_ReturnsTrue() = async { + let attr = DependsOnModulesWithTagAttribute("database") + let context = MockDependencyContext().WithTags(typeof, "database") + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsTrue()) + } + + [] + member _.ShouldDependOn_ModuleLacksTag_ReturnsFalse() = async { + let attr = DependsOnModulesWithTagAttribute("database") + let context = MockDependencyContext().WithTags(typeof, "other") + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsFalse()) + } + + [] + member _.ShouldDependOn_ModuleHasNoTags_ReturnsFalse() = async { + let attr = DependsOnModulesWithTagAttribute("database") + let context = MockDependencyContext() + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsFalse()) + } + + [] + member _.ShouldDependOn_CaseInsensitive_ReturnsTrue() = async { + let attr = DependsOnModulesWithTagAttribute("DATABASE") + let context = MockDependencyContext().WithTags(typeof, "database") + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsTrue()) + } + + [] + member _.ShouldDependOn_ModuleHasMultipleTagsIncludingMatch_ReturnsTrue() = async { + let attr = DependsOnModulesWithTagAttribute("database") + let context = MockDependencyContext().WithTags(typeof, "infrastructure", "database", "critical") + let result = attr.ShouldDependOn(typeof, context) + do! check(Assert.That(result).IsTrue()) + } + + [] + member _.Constructor_WithNullTag_ThrowsArgumentException() = async { + let mutable threw = false + + try + DependsOnModulesWithTagAttribute(null) |> ignore + with :? ArgumentException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.Constructor_WithEmptyTag_ThrowsArgumentException() = async { + let mutable threw = false + + try + DependsOnModulesWithTagAttribute(String.Empty) |> ignore + with :? ArgumentException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.Constructor_WithWhitespaceTag_ThrowsArgumentException() = async { + let mutable threw = false + + try + DependsOnModulesWithTagAttribute(" ") |> ignore + with :? ArgumentException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.Tag_Property_ReturnsConstructorValue() = async { + let attr = DependsOnModulesWithTagAttribute("my-tag") + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(attr.Tag), "my-tag")) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnTests.fs new file mode 100644 index 0000000000..a6098f7ace --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/DependsOnTests.fs @@ -0,0 +1,242 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System.Threading.Tasks +open ModularPipelines.Attributes +open ModularPipelines.Context +open ModularPipelines.Enums +open ModularPipelines.Exceptions +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type DependsOnTestModule1() = + inherit SimpleTestModule() + override _.Result = true + +[)>] +type DependsOnTestModule2() = + inherit SimpleTestModule() + override _.Result = true + +[, Optional = true)>] +type DependsOnTestModule3() = + inherit SimpleTestModule() + override _.Result = true + +[, Optional = true)>] +type DependsOnTestModule3WithGetIfRegistered() = + inherit Module() + + override _.ExecuteAsync(context, _) = + task { + let _ = context.GetModuleIfRegistered() + do! Task.Yield() + return true + } + +[, Optional = true)>] +type DependsOnTestModule3WithGet() = + inherit Module() + + override _.ExecuteAsync(context, _) = + task { + let module1 = context.GetModule() + let! _ = module1.CompletionSource.Task + do! Task.Yield() + return true + } + +[)>] +type DependsOnSelfModule() = + inherit Module() + + override _.ExecuteAsync(context, _) = + task { + let module1 = context.GetModule() + let! _ = module1.CompletionSource.Task + do! Task.Yield() + return true + } + +[)>] +type DependsOnNonModule() = + inherit Module() + + override _.ExecuteAsync(context, _) = + task { + let module1 = context.GetModule() + let! _ = module1.CompletionSource.Task + do! Task.Yield() + return true + } + +[, Optional = true)>] +type DependsOnTestModuleWithOptionalDep() = + inherit SimpleTestModule() + override _.Result = true + +[)>] +type DependsOnTestModuleWithRequiredDep() = + inherit SimpleTestModule() + override _.Result = true + +[, Optional = true)>] +type DependsOnTestModuleCheckingUnregisteredDep() = + inherit Module() + + override _.ExecuteAsync(context, _) = + task { + let dep = context.GetModuleIfRegistered() + do! Task.Yield() + return isNull dep + } + +type DependsOnTests() = + inherit TestBase() + + [] + member _.No_Exception_Thrown_When_Dependent_Module_Present() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.No_Exception_Thrown_When_Dependent_Module_Present_With_Optional() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.Required_Dependency_Is_Auto_Registered_When_Missing() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(Seq.length pipelineSummary.Modules), 2)) + } + + [] + member _.Optional_Dependency_Not_Auto_Registered_When_Missing() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(Seq.length pipelineSummary.Modules), 1)) + } + + [] + member _.No_Exception_Thrown_When_Optional_Dependency_Missing_And_Get_If_Registered_Called() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.Exception_Thrown_When_Optional_Dependency_Missing_And_Get_Module_Called() = async { + let mutable threw = false + + try + let! _ = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + () + with _ -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.Depends_On_Self_Module_Throws_Exception() = async { + let mutable threw = false + + try + let! _ = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + () + with :? ModuleReferencingSelfException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.Depends_On_Non_Module_Throws_Exception() = async { + let mutable message = null + + try + let! _ = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + () + with :? InvalidModuleTypeException as ex -> + message <- ex.Message + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(message), "ModularPipelines.Exceptions.ModuleFailedException is not a Module (does not implement IModule)")) + } + + [] + member _.Optional_Dependency_Works_When_Missing() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.Required_Dependency_Auto_Registers_Missing_Module() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + do! check(Assert.That(pipelineSummary.Modules |> Seq.exists (fun moduleInstance -> moduleInstance :? DependsOnTestModule1)).IsTrue()) + } + + [] + member _.Optional_Dependency_Returns_Null_When_GetModuleIfRegistered_Called_On_Unregistered() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/DirectCollisionTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/DirectCollisionTests.fs new file mode 100644 index 0000000000..8a98d86565 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/DirectCollisionTests.fs @@ -0,0 +1,47 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System.Threading.Tasks +open ModularPipelines.Attributes +open ModularPipelines.Context +open ModularPipelines.Exceptions +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +[)>] +type DependencyConflictModule1() = + inherit Module() + + override _.ExecuteAsync(context, _) = + task { + let module2 = context.GetModule() + let! _ = module2.CompletionSource.Task + do! Task.Yield() + return true + } + +and [)>] DependencyConflictModule2() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + +type DirectCollisionTests() = + [] + member _.Modules_Dependent_On_Each_Other_Throws_Exception() = async { + let mutable message = null + + try + let! _ = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + () + with :? DependencyCollisionException as ex -> + message <- ex.Message + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(message), "Dependency collision detected: **DependencyConflictModule1** -> DependencyConflictModule2 -> **DependencyConflictModule1**")) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/DynamicDependencyDeclarationTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/DynamicDependencyDeclarationTests.fs new file mode 100644 index 0000000000..47c934aa1d --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/DynamicDependencyDeclarationTests.fs @@ -0,0 +1,464 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System +open System.Threading +open System.Threading.Tasks +open ModularPipelines.Attributes +open ModularPipelines.Context +open ModularPipelines.Enums +open ModularPipelines.Exceptions +open ModularPipelines.Extensions +open ModularPipelines.Models +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module DynamicDependencyDeclarationTestsData = + type BaseModule() = + inherit SimpleTestModule() + override _.Result = "base" + + type OptionalDependencyModule() = + inherit SimpleTestModule() + override _.Result = "optional" + + type LazyModule() = + inherit SimpleTestModule() + override _.Result = "lazy" + + type ConditionalModule() = + inherit SimpleTestModule() + override _.Result = "conditional" + + type ModuleWithProgrammaticDependency() = + inherit Module() + + override _.DeclareDependencies(deps: IDependencyDeclaration) = + deps.DependsOn() |> ignore + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + return "programmatic" + } + + type ModuleWithOptionalDependency() = + inherit Module() + + override _.DeclareDependencies(deps: IDependencyDeclaration) = + deps.DependsOnOptional() |> ignore + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + return "optional-dep" + } + + type ModuleWithActiveConditionalDependency() = + inherit Module() + + override _.DeclareDependencies(deps: IDependencyDeclaration) = + deps.DependsOnIf(true) |> ignore + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + return "conditional-active" + } + + type ModuleWithInactiveConditionalDependency() = + inherit Module() + + override _.DeclareDependencies(deps: IDependencyDeclaration) = + deps.DependsOnIf(false) |> ignore + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + return "conditional-inactive" + } + + type ModuleWithPredicateConditionalDependency() = + inherit Module() + + static member val ShouldDependOnConditional = true with get, set + + override _.DeclareDependencies(deps: IDependencyDeclaration) = + deps.DependsOnIf(fun () -> ModuleWithPredicateConditionalDependency.ShouldDependOnConditional) + |> ignore + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + return "predicate-conditional" + } + + type ModuleWithLazyDependency() = + inherit Module() + + override _.DeclareDependencies(deps: IDependencyDeclaration) = + deps.DependsOnLazy() |> ignore + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + return "lazy-dep" + } + + [)>] + type ModuleWithBothAttributeAndProgrammaticDependencies() = + inherit Module() + + override _.DeclareDependencies(deps: IDependencyDeclaration) = + deps.DependsOnOptional() |> ignore + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + return "combined" + } + + type ModuleWithChainedDependencies() = + inherit Module() + + override _.DeclareDependencies(deps: IDependencyDeclaration) = + deps + .DependsOn() + .DependsOnOptional() + .DependsOnLazy() + |> ignore + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + return "chained" + } + + type ModuleWithMissingRequiredDependency() = + inherit Module() + + override _.DeclareDependencies(deps: IDependencyDeclaration) = + deps.DependsOn() |> ignore + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + return "missing-dep" + } + + type ModuleWithTypeDependency() = + inherit Module() + + override _.DeclareDependencies(deps: IDependencyDeclaration) = + deps.DependsOn(typeof) |> ignore + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + return "type-dep" + } + +type DynamicDependencyDeclarationTests() = + inherit TestBase() + + [] + member _.Programmatic_Required_Dependency_Works_When_Registered() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.Programmatic_Required_Dependency_Throws_When_Not_Registered() = async { + let mutable threw = false + + try + let! _ = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + () + with _ -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.Programmatic_Type_Dependency_Works() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.Optional_Dependency_Works_When_Registered() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.Optional_Dependency_Does_Not_Fail_When_Not_Registered() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.Conditional_Dependency_Works_When_Condition_True_And_Registered() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.Conditional_Dependency_Throws_When_Condition_True_And_Not_Registered() = async { + let mutable threw = false + + try + let! _ = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + () + with _ -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.Conditional_Dependency_Not_Added_When_Condition_False() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + [] + member _.Conditional_Predicate_Dependency_Works_When_Predicate_Returns_True() = async { + DynamicDependencyDeclarationTestsData.ModuleWithPredicateConditionalDependency.ShouldDependOnConditional <- true + + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + [] + member _.Conditional_Predicate_Dependency_Not_Added_When_Predicate_Returns_False() = async { + DynamicDependencyDeclarationTestsData.ModuleWithPredicateConditionalDependency.ShouldDependOnConditional <- false + + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.Lazy_Dependency_Does_Not_Fail_When_Not_Registered() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.Lazy_Dependency_Works_When_Registered() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.Combined_Attribute_And_Programmatic_Dependencies_Work() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.Combined_Dependencies_Work_With_Only_Attribute_Dependency_Registered() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.Chained_Dependency_Declarations_Work() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.Chained_Dependency_Declarations_Work_With_Only_Required_Registered() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.DependencyDeclaration_DependsOn_Returns_Same_Instance_For_Chaining() = async { + let declaration = DependencyDeclaration() + let result = declaration.DependsOn() + do! check(Assert.That(Object.ReferenceEquals(result, declaration)).IsTrue()) + } + + [] + member _.DependencyDeclaration_DependsOnOptional_Returns_Same_Instance_For_Chaining() = async { + let declaration = DependencyDeclaration() + let result = declaration.DependsOnOptional() + do! check(Assert.That(Object.ReferenceEquals(result, declaration)).IsTrue()) + } + + [] + member _.DependencyDeclaration_DependsOnIf_Returns_Same_Instance_For_Chaining() = async { + let declaration = DependencyDeclaration() + let result = declaration.DependsOnIf(true) + do! check(Assert.That(Object.ReferenceEquals(result, declaration)).IsTrue()) + } + + [] + member _.DependencyDeclaration_DependsOnLazy_Returns_Same_Instance_For_Chaining() = async { + let declaration = DependencyDeclaration() + let result = declaration.DependsOnLazy() + do! check(Assert.That(Object.ReferenceEquals(result, declaration)).IsTrue()) + } + + [] + member _.DependencyDeclaration_Throws_For_Non_Module_Type() = async { + let declaration = DependencyDeclaration() + let mutable message = null + + try + declaration.DependsOn(typeof) |> ignore + with :? InvalidModuleTypeException as ex -> + message <- ex.Message + + do! check(Assert.That(message <> null).IsTrue()) + do! check(Assert.That(message.Contains("is not a Module")).IsTrue()) + } + + [] + member _.DependencyDeclaration_Required_Has_Correct_DependencyType() = async { + let declaration = DependencyDeclaration() + declaration.DependsOn() |> ignore + let deps = declaration.Dependencies + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(deps.Count), 1)) + do! check(Assert.That(deps.[0].Kind).IsEqualTo(DependencyType.Required)) + do! check(Assert.That(deps.[0].IsOptional).IsEqualTo(false)) + } + + [] + member _.DependencyDeclaration_Optional_Has_Correct_DependencyType() = async { + let declaration = DependencyDeclaration() + declaration.DependsOnOptional() |> ignore + let deps = declaration.Dependencies + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(deps.Count), 1)) + do! check(Assert.That(deps.[0].Kind).IsEqualTo(DependencyType.Optional)) + do! check(Assert.That(deps.[0].IsOptional).IsEqualTo(true)) + } + + [] + member _.DependencyDeclaration_Lazy_Has_Correct_DependencyType() = async { + let declaration = DependencyDeclaration() + declaration.DependsOnLazy() |> ignore + let deps = declaration.Dependencies + + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(deps.Count), 1)) + do! check(Assert.That(deps.[0].Kind).IsEqualTo(DependencyType.Lazy)) + do! check(Assert.That(deps.[0].IsOptional).IsEqualTo(true)) + } + + [] + member _.DependencyDeclaration_Conditional_Has_Correct_DependencyType() = async { + let declaration = DependencyDeclaration() + declaration.DependsOnIf(true) |> ignore + let deps = declaration.Dependencies + + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(deps.Count), 1)) + do! check(Assert.That(deps.[0].Kind).IsEqualTo(DependencyType.Conditional)) + do! check(Assert.That(deps.[0].IsOptional).IsEqualTo(false)) + } + + [] + member _.DependencyDeclaration_Conditional_False_Does_Not_Add_Dependency() = async { + let declaration = DependencyDeclaration() + declaration.DependsOnIf(false) |> ignore + let deps = declaration.Dependencies + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(deps.Count), 0)) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/FlexibleDependencyIntegrationTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/FlexibleDependencyIntegrationTests.fs new file mode 100644 index 0000000000..2fe92058d5 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/FlexibleDependencyIntegrationTests.fs @@ -0,0 +1,558 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +//open System +//open System.Collections.Concurrent +//open System.Collections.Generic +//open System.Threading +//open System.Threading.Tasks +//open Microsoft.Extensions.DependencyInjection +//open ModularPipelines.Attributes +//open ModularPipelines.Context +//open ModularPipelines.Enums +//open ModularPipelines.Extensions +//open ModularPipelines.Modules +//open ModularPipelines.TestHelpers +//open TUnit.Assertions +//open TUnit.Assertions.Extensions +//open TUnit.Assertions.FSharp.Operations +//open TUnit.Core + +//module FlexibleDependencyIntegrationTestsData = +// let executionOrderQueue = ConcurrentQueue() + +// let clearExecutionOrder() = executionOrderQueue.Clear() + +// let getExecutionOrder() = executionOrderQueue.ToArray() |> Array.toList + +// let recordExecution moduleName = executionOrderQueue.Enqueue(moduleName) + +// [] +// type CriticalAttribute() = +// inherit Attribute() + +// [] +// type DatabaseModuleA() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof DatabaseModuleA) +// return "DatabaseA" +// } + +// [] +// type DatabaseModuleB() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof DatabaseModuleB) +// return "DatabaseB" +// } + +// [] +// type NonDatabaseModule() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof NonDatabaseModule) +// return "NonDatabase" +// } + +// [] +// type AfterDatabaseModule() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof AfterDatabaseModule) +// return "AfterDatabase" +// } + +// [] +// type ModuleDependingOnNonExistentTag() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof ModuleDependingOnNonExistentTag) +// return "DependsOnNonExistent" +// } + +// [] +// [] +// [] +// type ModuleWithMultipleTags() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof ModuleWithMultipleTags) +// return "MultipleTags" +// } + +// [] +// type AfterSlowModule() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof AfterSlowModule) +// return "AfterSlow" +// } + +// [] +// type InfrastructureModuleA() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof InfrastructureModuleA) +// return "InfrastructureA" +// } + +// [] +// type InfrastructureModuleB() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof InfrastructureModuleB) +// return "InfrastructureB" +// } + +// [] +// type BuildModule() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof BuildModule) +// return "Build" +// } + +// [] +// type AfterInfrastructureModule() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof AfterInfrastructureModule) +// return "AfterInfrastructure" +// } + +// [] +// type ModuleDependingOnNonExistentCategory() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof ModuleDependingOnNonExistentCategory) +// return "DependsOnNonExistentCategory" +// } + +// [] +// type CriticalModuleA() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof CriticalModuleA) +// return "CriticalA" +// } + +// [] +// type CriticalModuleB() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof CriticalModuleB) +// return "CriticalB" +// } + +// type NonCriticalModule() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof NonCriticalModule) +// return "NonCritical" +// } + +// [] +// type BaseCriticalModule() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof BaseCriticalModule) +// return "BaseCritical" +// } + +// type DerivedCriticalModule() = +// inherit BaseCriticalModule() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof DerivedCriticalModule) +// return "DerivedCritical" +// } + +// [>] +// type AfterCriticalModule() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof AfterCriticalModule) +// return "AfterCritical" +// } + +// type ModuleWithOverrideTags() = +// inherit Module() +// override _.Tags = HashSet([ "database"; "override-tag" ]) :> IReadOnlySet +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof ModuleWithOverrideTags) +// return "OverrideTags" +// } + +// type ModuleWithOverrideCategory() = +// inherit Module() +// override _.Category = "infrastructure" +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof ModuleWithOverrideCategory) +// return "OverrideCategory" +// } + +// type PlainModule() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof PlainModule) +// return "Plain" +// } + +// [] +// [] +// [] +// type ModuleWithMultipleFlexibleDependencies() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof ModuleWithMultipleFlexibleDependencies) +// return "MultipleFlexibleDeps" +// } + +// [] +// [] +// [] +// type AfterDatabaseModuleWithPhase1Tag() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof AfterDatabaseModuleWithPhase1Tag) +// return "AfterDbWithPhase1" +// } + +// [] +// type AfterPhase1Module() = +// inherit Module() +// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = +// task { +// do! Task.Yield() +// recordExecution (nameof AfterPhase1Module) +// return "AfterPhase1" +// } + +//[] +//type FlexibleDependencyIntegrationTests() = +// inherit TestBase() + +// [] +// member _.Setup() = +// FlexibleDependencyIntegrationTestsData.clearExecutionOrder() + +// [] +// member _.DependsOnModulesWithTag_WaitsForTaggedModules() = async { +// let! result = +// TestPipelineHostBuilder.Create() +// .AddModule() +// .AddModule() +// .AddModule() +// .AddModule() +// .ExecutePipelineAsync() +// |> Async.AwaitTask + +// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + +// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() +// let afterDbIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterDatabaseModule) +// let dbAIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.DatabaseModuleA) +// let dbBIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.DatabaseModuleB) + +// do! check(Assert.That(afterDbIndex).IsGreaterThan(dbAIndex)) +// do! check(Assert.That(afterDbIndex).IsGreaterThan(dbBIndex)) +// } + +// [] +// member _.DependsOnModulesWithTag_NoMatchingModules_StillSucceeds() = async { +// let! result = +// TestPipelineHostBuilder.Create() +// .AddModule() +// .AddModule() +// .ExecutePipelineAsync() +// |> Async.AwaitTask + +// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) +// } + +// [] +// member _.DependsOnModulesWithTag_MultipleTagsOnModule_MatchesCorrectly() = async { +// let! result = +// TestPipelineHostBuilder.Create() +// .AddModule() +// .AddModule() +// .ExecutePipelineAsync() +// |> Async.AwaitTask + +// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + +// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() +// let multiTagIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.ModuleWithMultipleTags) +// let afterSlowIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterSlowModule) + +// do! check(Assert.That(afterSlowIndex).IsGreaterThan(multiTagIndex)) +// } + +// [] +// member _.DependsOnModulesInCategory_WaitsForCategorizedModules() = async { +// let! result = +// TestPipelineHostBuilder.Create() +// .AddModule() +// .AddModule() +// .AddModule() +// .AddModule() +// .ExecutePipelineAsync() +// |> Async.AwaitTask + +// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + +// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() +// let afterInfraIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterInfrastructureModule) +// let infraAIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.InfrastructureModuleA) +// let infraBIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.InfrastructureModuleB) + +// do! check(Assert.That(afterInfraIndex).IsGreaterThan(infraAIndex)) +// do! check(Assert.That(afterInfraIndex).IsGreaterThan(infraBIndex)) +// } + +// [] +// member _.DependsOnModulesInCategory_NoMatchingModules_StillSucceeds() = async { +// let! result = +// TestPipelineHostBuilder.Create() +// .AddModule() +// .AddModule() +// .ExecutePipelineAsync() +// |> Async.AwaitTask + +// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) +// } + +// [] +// member _.DependsOnModulesWithAttribute_WaitsForAttributedModules() = async { +// let! result = +// TestPipelineHostBuilder.Create() +// .AddModule() +// .AddModule() +// .AddModule() +// .AddModule() +// .ExecutePipelineAsync() +// |> Async.AwaitTask + +// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + +// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() +// let afterCriticalIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterCriticalModule) +// let criticalAIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.CriticalModuleA) +// let criticalBIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.CriticalModuleB) + +// do! check(Assert.That(afterCriticalIndex).IsGreaterThan(criticalAIndex)) +// do! check(Assert.That(afterCriticalIndex).IsGreaterThan(criticalBIndex)) +// } + +// [] +// member _.DependsOnModulesWithAttribute_InheritedAttribute_IsRecognized() = async { +// let! result = +// TestPipelineHostBuilder.Create() +// .AddModule() +// .AddModule() +// .ExecutePipelineAsync() +// |> Async.AwaitTask + +// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + +// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() +// let derivedIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.DerivedCriticalModule) +// let afterCriticalIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterCriticalModule) + +// do! check(Assert.That(afterCriticalIndex).IsGreaterThan(derivedIndex)) +// } + +// [] +// member _.DependsOnModulesWithAttribute_NoMatchingModules_StillSucceeds() = async { +// let! result = +// TestPipelineHostBuilder.Create() +// .AddModule() +// .AddModule() +// .ExecutePipelineAsync() +// |> Async.AwaitTask + +// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) +// } + +// [] +// member _.ModuleWithOverrideTags_IsRecognizedByTagDependency() = async { +// let! result = +// TestPipelineHostBuilder.Create() +// .AddModule() +// .AddModule() +// .ExecutePipelineAsync() +// |> Async.AwaitTask + +// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + +// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() +// let overrideIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.ModuleWithOverrideTags) +// let afterDbIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterDatabaseModule) + +// do! check(Assert.That(afterDbIndex).IsGreaterThan(overrideIndex)) +// } + +// [] +// member _.ModuleWithOverrideCategory_IsRecognizedByCategoryDependency() = async { +// let! result = +// TestPipelineHostBuilder.Create() +// .AddModule() +// .AddModule() +// .ExecutePipelineAsync() +// |> Async.AwaitTask + +// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + +// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() +// let overrideIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.ModuleWithOverrideCategory) +// let afterInfraIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterInfrastructureModule) + +// do! check(Assert.That(afterInfraIndex).IsGreaterThan(overrideIndex)) +// } + +// [] +// member _.ModuleWithRegistrationTags_IsRecognizedByTagDependency() = async { +// let! result = +// TestPipelineHostBuilder.Create() +// .ConfigureServices(fun _ services -> +// services.AddModule().WithTags("database") |> ignore) +// .AddModule() +// .ExecutePipelineAsync() +// |> Async.AwaitTask + +// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + +// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() +// let plainIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.PlainModule) +// let afterDbIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterDatabaseModule) + +// do! check(Assert.That(afterDbIndex).IsGreaterThan(plainIndex)) +// } + +// [] +// member _.ModuleWithRegistrationCategory_IsRecognizedByCategoryDependency() = async { +// let! result = +// TestPipelineHostBuilder.Create() +// .ConfigureServices(fun _ services -> +// services.AddModule().WithCategory("infrastructure") |> ignore) +// .AddModule() +// .ExecutePipelineAsync() +// |> Async.AwaitTask + +// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + +// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() +// let plainIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.PlainModule) +// let afterInfraIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterInfrastructureModule) + +// do! check(Assert.That(afterInfraIndex).IsGreaterThan(plainIndex)) +// } + +// [] +// member _.ModuleWithBothAttributeAndRegistrationTags_MergesTags() = async { +// let! result = +// TestPipelineHostBuilder.Create() +// .ConfigureServices(fun _ services -> +// services.AddModule().WithTags("slow") |> ignore) +// .AddModule> +// .ExecutePipelineAsync() +// |> Async.AwaitTask + +// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + +// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() +// let dbAIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.DatabaseModuleA) +// let afterSlowIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterSlowModule) + +// do! check(Assert.That(afterSlowIndex).IsGreaterThan(dbAIndex)) +// } + +// [] +// member _.CombinedDependencies_ModuleWithMultipleFlexibleDependencies() = async { +// let! result = +// TestPipelineHostBuilder.Create() +// .AddModule() +// .AddModule() +// .AddModule() +// .AddModule() +// .ExecutePipelineAsync() +// |> Async.AwaitTask + +// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + +// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() +// let combinedIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.ModuleWithMultipleFlexibleDependencies) +// let dbAIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.DatabaseModuleA) +// let infraAIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.InfrastructureModuleA) +// let criticalAIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.CriticalModuleA) + +// do! check(Assert.That(combinedIndex).IsGreaterThan(dbAIndex)) +// do! check(Assert.That(combinedIndex).IsGreaterThan(infraAIndex)) +// do! check(Assert.That(combinedIndex).IsGreaterThan(criticalAIndex)) +// } + +// [] +// member _.ChainedFlexibleDependencies_ExecuteInCorrectOrder() = async { +// let! result = +// TestPipelineHostBuilder.Create() +// .AddModule() +// .AddModule() +// .AddModule() +// .ExecutePipelineAsync() +// |> Async.AwaitTask + +// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + +// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() +// let dbAIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.DatabaseModuleA) +// let afterDbIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterDatabaseModuleWithPhase1Tag) +// let afterPhase1Index = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterPhase1Module) + +// do! check(Assert.That(afterDbIndex).IsGreaterThan(dbAIndex)) +// do! check(Assert.That(afterPhase1Index).IsGreaterThan(afterDbIndex)) +// } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/MockDependencyContext.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/MockDependencyContext.fs new file mode 100644 index 0000000000..d6019a2cbd --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/MockDependencyContext.fs @@ -0,0 +1,53 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System +open System.Collections.Generic +open System.Reflection +open ModularPipelines.Context + +/// +/// A mock implementation of for unit testing dependency attributes. +/// +type MockDependencyContext() = + let tags = Dictionary>() + let categories = Dictionary() + + /// + /// Configures tags for a specific module type. + /// + /// The module type to configure tags for. + /// The tags to associate with the module type. + /// This instance for method chaining. + member this.WithTags(moduleType: Type, [] tagArray: string[]) = + tags[moduleType] <- HashSet(tagArray, StringComparer.OrdinalIgnoreCase) + this + + /// + /// Configures the category for a specific module type. + /// + /// The module type to configure the category for. + /// The category to associate with the module type. + /// This instance for method chaining. + member this.WithCategory(moduleType: Type, category: string) = + categories[moduleType] <- category + this + + interface IDependencyContext with + member _.GetTags(moduleType: Type) = + match tags.TryGetValue(moduleType) with + | true, t -> t :> IReadOnlySet + | false, _ -> HashSet() :> IReadOnlySet + + member _.GetCategory(moduleType: Type) = + match categories.TryGetValue(moduleType) with + | true, cat -> cat + | false, _ -> null + + member _.HasAttribute<'TAttribute when 'TAttribute :> Attribute>(moduleType: Type) = + moduleType.GetCustomAttributes(typeof<'TAttribute>, true).Length > 0 + + member _.GetAttribute<'TAttribute when 'TAttribute :> Attribute>(moduleType: Type) = + moduleType.GetCustomAttribute<'TAttribute>() + + member _.GetAttributes<'TAttribute when 'TAttribute :> Attribute>(moduleType: Type) = + moduleType.GetCustomAttributes<'TAttribute>() diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/ModuleCategoryAttributeTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/ModuleCategoryAttributeTests.fs new file mode 100644 index 0000000000..7a1e1d8f08 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/ModuleCategoryAttributeTests.fs @@ -0,0 +1,52 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System +open System.Reflection +open ModularPipelines.Attributes +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type ModuleCategoryAttributeTests() = + [] + member _.Constructor_WithValidCategory_SetsCategoryProperty() = async { + let attr = ModuleCategoryAttribute("infrastructure") + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(attr.Category), "infrastructure")) + } + + [] + member _.Constructor_WithNullCategory_ThrowsArgumentException() = async { + let mutable threw = false + + try + ModuleCategoryAttribute(null) |> ignore + with :? ArgumentException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.Constructor_WithEmptyCategory_ThrowsArgumentException() = async { + let mutable threw = false + + try + ModuleCategoryAttribute(String.Empty) |> ignore + with :? ArgumentException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.Attribute_DoesNotAllowMultiple() = async { + let usage = typeof.GetCustomAttribute() + do! check(Assert.That(usage.AllowMultiple).IsFalse()) + } + + [] + member _.Attribute_IsInheritable() = async { + let usage = typeof.GetCustomAttribute() + do! check(Assert.That(usage.Inherited).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/ModuleNotRegisteredExceptionTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/ModuleNotRegisteredExceptionTests.fs new file mode 100644 index 0000000000..a41080f164 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/ModuleNotRegisteredExceptionTests.fs @@ -0,0 +1,89 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System.Threading +open System.Threading.Tasks +open ModularPipelines.Attributes +open ModularPipelines.Context +open ModularPipelines.Enums +open ModularPipelines.Exceptions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open ModularPipelines.Extensions + +[, Optional = true)>] +type Module2WithOptionalDep() = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, _: CancellationToken) = + task { + let! _ = context.GetModule() + do! Task.Yield() + return true + } + +and Module1() = + inherit Module() + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + return true + } + +[)>] +type Module2WithRequiredDep() = + inherit Module() + + override _.ExecuteAsync(context: IModuleContext, _: CancellationToken) = + task { + let! _ = context.GetModule() + do! Task.Yield() + return true + } + +type ModuleNotRegisteredExceptionTests() = + inherit TestBase() + + [] + member _.Module_Getting_Non_Registered_Module_With_Optional_Dep_Throws_ModuleFailedException() = async { + let mutable threw = false + + try + let! _ = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + () + with :? ModuleFailedException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.Module_With_Required_Dependency_Auto_Registers_Missing_Module() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(pipelineSummary.Modules |> Seq.length), 2)) + } + + [] + member _.Module_Getting_Registered_Module_Does_Not_Throw_Exception() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/ModuleTagAttributeTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/ModuleTagAttributeTests.fs new file mode 100644 index 0000000000..fb24f02a48 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/ModuleTagAttributeTests.fs @@ -0,0 +1,64 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System +open System.Reflection +open ModularPipelines.Attributes +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type ModuleTagAttributeTests() = + [] + member _.Constructor_WithValidTag_SetsTagProperty() = async { + let attr = ModuleTagAttribute("database") + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(attr.Tag), "database")) + } + + [] + member _.Constructor_WithNullTag_ThrowsArgumentException() = async { + let mutable threw = false + + try + ModuleTagAttribute(null) |> ignore + with :? ArgumentException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.Constructor_WithEmptyTag_ThrowsArgumentException() = async { + let mutable threw = false + + try + ModuleTagAttribute(String.Empty) |> ignore + with :? ArgumentException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.Constructor_WithWhitespaceTag_ThrowsArgumentException() = async { + let mutable threw = false + + try + ModuleTagAttribute(" ") |> ignore + with :? ArgumentException -> + threw <- true + + do! check(Assert.That(threw).IsTrue()) + } + + [] + member _.Attribute_AllowsMultiple() = async { + let usage = typeof.GetCustomAttribute() + do! check(Assert.That(usage.AllowMultiple).IsTrue()) + } + + [] + member _.Attribute_IsInheritable() = async { + let usage = typeof.GetCustomAttribute() + do! check(Assert.That(usage.Inherited).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/NestedCollisionTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/NestedCollisionTests.fs new file mode 100644 index 0000000000..877bd99eaa --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/NestedCollisionTests.fs @@ -0,0 +1,56 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System.Threading.Tasks +open ModularPipelines.Attributes +open ModularPipelines.Context +open ModularPipelines.Exceptions +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module NestedCollisionTestModules = + [)>] + type DependencyConflictModule1() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + + and [)>] DependencyConflictModule2() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + + and [)>] DependencyConflictModule3() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + + and [)>] DependencyConflictModule4() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + + and [)>] DependencyConflictModule5() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + +type NestedCollisionTests() = + [] + member _.Modules_Dependent_On_Each_Other_Throws_Exception() = async { + let mutable message = null + + try + let! _ = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .AddModule() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + () + with :? DependencyCollisionException as ex -> + message <- ex.Message + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(message), "Dependency collision detected: **DependencyConflictModule2** -> DependencyConflictModule3 -> DependencyConflictModule4 -> DependencyConflictModule5 -> **DependencyConflictModule2**")) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/OneWayDependenciesNonCollisionTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/OneWayDependenciesNonCollisionTests.fs new file mode 100644 index 0000000000..3ab6601bed --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/OneWayDependenciesNonCollisionTests.fs @@ -0,0 +1,51 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System.Threading.Tasks +open ModularPipelines.Attributes +open ModularPipelines.Context +open ModularPipelines.Enums +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module OneWayDependenciesNonCollisionTestModules = + [)>] + type DependencyConflictModule1() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + + and [)>] DependencyConflictModule2() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + + and [)>] DependencyConflictModule3() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + + and [)>] DependencyConflictModule4() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + + and DependencyConflictModule5() = + inherit Module() + override _.ExecuteAsync(_, _) = Task.FromResult(true) + +type OneWayDependenciesNonCollisionTests() = + [] + member _.Modules_Not_Dependent_On_Each_Other_Succeed() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .AddModule() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/SingleTypeParameterGetModuleTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/SingleTypeParameterGetModuleTests.fs new file mode 100644 index 0000000000..618a57a8f6 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/SingleTypeParameterGetModuleTests.fs @@ -0,0 +1,185 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System.Threading.Tasks +open ModularPipelines.Attributes +open ModularPipelines.Context +open ModularPipelines.Enums +open ModularPipelines.Exceptions +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module SingleTypeParameterGetModuleTestModules = + type ComplexResult = { + Id: int + Name: string + } + + type StringModule() = + inherit Module() + + override _.ExecuteAsync(_, _) = + task { + do! Task.Yield() + return "Hello from StringModule" + } + + type ComplexResultModule() = + inherit Module() + + override _.ExecuteAsync(_, _) = + task { + do! Task.Yield() + return { Id = 42; Name = "Test" } + } + + [)>] + type ConsumerModule() = + inherit Module() + + override _.ExecuteAsync(context, _) = + task { + let! result = context.GetModule() + + if result.IsSuccess then + return result.ValueOrDefault + else + return "failed" + } + + [)>] + type ComplexConsumerModule() = + inherit Module() + + override _.ExecuteAsync(context, _) = + task { + let! result = context.GetModule() + + if result.IsSuccess && not (isNull result.ValueOrDefault) then + return result.ValueOrDefault.Id + else + return -1 + } + + [, Optional = true)>] + type OptionalConsumerModule() = + inherit Module() + + override _.ExecuteAsync(context, _) = + task { + let moduleInstance = context.GetModuleIfRegistered() + + if isNull moduleInstance then + return "not registered" + else + let! result = moduleInstance + return if isNull result.ValueOrDefault then "default" else result.ValueOrDefault + } + + type SelfReferencingModule() = + inherit Module() + + override _.ExecuteAsync(context, _) = + task { + let _ = context.GetModule() + do! Task.Yield() + return true + } + + type UnregisteredConsumerModule() = + inherit Module() + + override _.ExecuteAsync(context, _) = + task { + let _ = context.GetModule() + do! Task.Yield() + return true + } + +type SingleTypeParameterGetModuleTests() = + inherit TestBase() + + [] + member _.GetModule_SingleTypeParameter_ReturnsCorrectlyTypedResult() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.GetModule_SingleTypeParameter_WithComplexType_InfersTypeCorrectly() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.GetModuleIfRegistered_SingleTypeParameter_ReturnsModule_WhenRegistered() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.GetModuleIfRegistered_SingleTypeParameter_ReturnsNull_WhenNotRegistered() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(pipelineSummary.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.GetModule_SingleTypeParameter_ThrowsModuleReferencingSelfException() = async { + let mutable innerException = null + + try + let! _ = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + () + with :? ModuleFailedException as ex -> + innerException <- ex.InnerException + + do! check(Assert.That(innerException).IsTypeOf()) + } + + [] + member _.GetModule_SingleTypeParameter_ThrowsModuleNotRegisteredException() = async { + let mutable innerException = null + + try + let! _ = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + () + with :? ModuleFailedException as ex -> + innerException <- ex.InnerException + + do! check(Assert.That(innerException).IsTypeOf()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/TimedDependencyTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/TimedDependencyTests.fs new file mode 100644 index 0000000000..46f1cbadff --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/TimedDependencyTests.fs @@ -0,0 +1,62 @@ +namespace ModularPipelines.UnitTests.FSharp.Dependencies + +open System +open System.Threading.Tasks +open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Time.Testing +open ModularPipelines.Attributes +open ModularPipelines.Context +open ModularPipelines.Engine +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module TimedDependencyTestModules = + let longModuleDelay = TimeSpan.FromMilliseconds(100:int64) + let shortModuleDelay = TimeSpan.FromMilliseconds(20:int64) + + type FiveSecondModule() = + inherit Module() + + override _.ExecuteAsync(_, cancellationToken) = + task { + do! Task.Delay(longModuleDelay, cancellationToken) + return true + } + + [)>] + type OneSecondModuleDependentOnFiveSecondModule() = + inherit Module() + + override _.ExecuteAsync(_, cancellationToken) = + task { + do! Task.Delay(shortModuleDelay, cancellationToken) + return true + } + +type TimedDependencyTests() = + [] + member _.OneSecondModule_WillWaitForFiveSecondModule_ThenExecute() = async { + let timeProvider = FakeTimeProvider() + + let! host = + TestPipelineHostBuilder.Create(TestHostSettings(), timeProvider) + .AddModule() + .AddModule() + .BuildHostAsync() + |> Async.AwaitTask + + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + + let resultRegistry = host.RootServices.GetRequiredService() + let fiveSecondResult = resultRegistry.GetResult(typeof) + let oneSecondResult = resultRegistry.GetResult(typeof) + + do! check(Assert.That(oneSecondResult.ModuleDuration >= TimedDependencyTestModules.shortModuleDelay.Add(TimeSpan.FromMilliseconds(-10:int64))).IsTrue()) + do! check(Assert.That(oneSecondResult.ModuleEnd >= fiveSecondResult.ModuleStart + TimedDependencyTestModules.longModuleDelay + TimedDependencyTestModules.shortModuleDelay.Add(TimeSpan.FromMilliseconds(-20:int64))).IsTrue()) + do! check(Assert.That(oneSecondResult.ModuleStart >= fiveSecondResult.ModuleEnd).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Engine/BuildSystemDetectorTests.fs b/test/ModularPipelines.UnitTests.FSharp/Engine/BuildSystemDetectorTests.fs new file mode 100644 index 0000000000..8240beb9a6 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Engine/BuildSystemDetectorTests.fs @@ -0,0 +1,72 @@ +namespace ModularPipelines.UnitTests.FSharp.Engine + +open System +open ModularPipelines +open ModularPipelines.Context +open ModularPipelines.Enums +open Moq +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type BuildSystemDetectorTests() = + + let environmentVariables = Mock() + let buildSystemDetector : IBuildSystemDetector = + BuildSystemDetector(environmentVariables.Object) + + let setupVar name = + environmentVariables + .Setup(fun x -> x.GetEnvironmentVariable(name, It.IsAny<_>())) + .Returns("dummy value") + |> ignore + + [] + member _.When_No_Known_BuildAgent_Variable_Then_IsKnownBuildAgent_Returns_False() = async { + do! check(Assert.That(buildSystemDetector.IsKnownBuildAgent).IsFalse()) + } + + [] + [] + [] + [] + [] + [] + [] + [] + [] + member _.When_Known_BuildAgent_Variable_Then_IsKnownBuildAgent_Returns_True(environmentVariableName: string) = async { + setupVar environmentVariableName + do! check(Assert.That(buildSystemDetector.IsKnownBuildAgent).IsTrue()) + } + + [] + member _.Each_Property_Returns_Result() = async { + do! check(Assert.That(buildSystemDetector.IsRunningOnBitbucket).IsFalse()) + do! check(Assert.That(buildSystemDetector.IsRunningOnJenkins).IsFalse()) + do! check(Assert.That(buildSystemDetector.IsRunningOnAzurePipelines).IsFalse()) + do! check(Assert.That(buildSystemDetector.IsRunningOnTeamCity).IsFalse()) + + // Faithful translation of: IsTrue().Or.IsFalse() + do! check(Assert.That(buildSystemDetector.IsRunningOnGitHubActions).IsTrue().Or.IsFalse()) + + do! check(Assert.That(buildSystemDetector.IsRunningOnAppVeyor).IsFalse()) + do! check(Assert.That(buildSystemDetector.IsRunningOnGitLab).IsFalse()) + do! check(Assert.That(buildSystemDetector.IsRunningOnTravisCI).IsFalse()) + } + + [] + [] + [] + [] + [] + [] + [] + [] + [] + [] + member _.Expected_Build_Agent(environmentVariableName: string, expectedBuildSystem: BuildSystem) = async { + setupVar environmentVariableName + do! check(Assert.That(buildSystemDetector.GetCurrentBuildSystem()).IsEqualTo(expectedBuildSystem)) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Engine/DependencyInjectionTests.fs b/test/ModularPipelines.UnitTests.FSharp/Engine/DependencyInjectionTests.fs new file mode 100644 index 0000000000..01c196167e --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Engine/DependencyInjectionTests.fs @@ -0,0 +1,66 @@ +namespace ModularPipelines.UnitTests.FSharp.Engine + +open Microsoft.Extensions.Configuration +open Microsoft.Extensions.DependencyInjection +open Microsoft.Extensions.Hosting +open ModularPipelines.Context +open ModularPipelines.Extensions +open ModularPipelines.DependencyInjection +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open Moq +open TUnit.Assertions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open TUnit.Assertions.Extensions + +type private DependencyInjectionModule() = + inherit Module() + + override _.ExecuteAsync(_: IModuleContext, _) = + System.Threading.Tasks.Task.FromResult(true) + +type DependencyInjectionTests() = + [] + member _.AllDependenciesCanBeBuilt() = async { + let! host = + TestPipelineHostBuilder.Create() + .AddModule() + .BuildHostAsync() + |> Async.AwaitTask + + let services = host.Services + let collection = services.GetRequiredService().ServiceCollection + + for serviceDescriptor in + collection + |> Seq.filter (fun sd -> + let serviceNamespace = sd.ServiceType.Namespace + not sd.ServiceType.IsGenericType + && not (isNull serviceNamespace) + && serviceNamespace.StartsWith("ModularPipeline")) do + services.GetRequiredService(serviceDescriptor.ServiceType) |> ignore + + do! check(Assert.That(true).IsTrue()) + } + + [] + member _.Validate() = async { + let serviceCollection = + ServiceCollection() + .AddSingleton(Mock.Of()) + .AddSingleton(Mock.Of()) + .AddSingleton(Mock.Of()) + + DependencyInjectionSetup.Initialize(serviceCollection) + + serviceCollection.BuildServiceProvider( + ServiceProviderOptions( + ValidateScopes = true, + ValidateOnBuild = true + ) + ) + |> ignore + + do! check(Assert.That(true).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Engine/DependencyTreeFormatterTests.fs b/test/ModularPipelines.UnitTests.FSharp/Engine/DependencyTreeFormatterTests.fs new file mode 100644 index 0000000000..00751e5033 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Engine/DependencyTreeFormatterTests.fs @@ -0,0 +1,157 @@ +namespace ModularPipelines.UnitTests.FSharp.Engine + +open System +open System.IO +open ModularPipelines.Context +open ModularPipelines.Engine +open ModularPipelines.Models +open ModularPipelines.Modules +open Spectre.Console +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +type private ModuleA() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _) = System.Threading.Tasks.Task.FromResult(true) + +type private ModuleB() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _) = System.Threading.Tasks.Task.FromResult(true) + +type private ModuleC() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _) = System.Threading.Tasks.Task.FromResult(true) + +type private ModuleD() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _) = System.Threading.Tasks.Task.FromResult(true) + +type private SharedModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _) = System.Threading.Tasks.Task.FromResult(true) + +[] +module private DependencyTreeFormatterTestHelpers = + let renderToString (tree: Tree) = + use writer = new StringWriter() + let settings = AnsiConsoleSettings() + settings.Out <- AnsiConsoleOutput(writer) + settings.Ansi <- AnsiSupport.No + let console = AnsiConsole.Create(settings) + console.Write(tree) + writer.ToString() + + let createModel<'T when 'T :> IModule and 'T : (new: unit -> 'T)> () = + ModuleDependencyModel(Activator.CreateInstance<'T>()) + +type DependencyTreeFormatterTests() = + [] + member _.FormatTree_SingleModule_NoDependencies_ReturnsTreeWithSingleNode() = async { + let formatter = DependencyTreeFormatter() + let moduleA = createModel() + let roots = [| moduleA |] + + let tree = formatter.FormatTree(roots) + let output = renderToString tree + + do! check(Assert.That(output.Contains("Module Dependencies")).IsTrue()) + do! check(Assert.That(output.Contains("ModuleA")).IsTrue()) + } + + [] + member _.FormatTree_LinearChain_ReturnsTreeWithCorrectHierarchy() = async { + let formatter = DependencyTreeFormatter() + let moduleA = createModel() + let moduleB = createModel() + let moduleC = createModel() + + moduleA.IsDependencyFor.Add(moduleB) + moduleB.IsDependencyFor.Add(moduleC) + + let tree = formatter.FormatTree([| moduleA |]) + let output = renderToString tree + + do! check(Assert.That(output.Contains("ModuleA")).IsTrue()) + do! check(Assert.That(output.Contains("ModuleB")).IsTrue()) + do! check(Assert.That(output.Contains("ModuleC")).IsTrue()) + } + + [] + member _.FormatTree_MultipleRoots_ReturnsTreeWithAllRoots() = async { + let formatter = DependencyTreeFormatter() + let moduleA = createModel() + let moduleB = createModel() + + let tree = formatter.FormatTree([| moduleA; moduleB |]) + let output = renderToString tree + + do! check(Assert.That(output.Contains("ModuleA")).IsTrue()) + do! check(Assert.That(output.Contains("ModuleB")).IsTrue()) + } + + [] + member _.FormatTree_SharedModule_MarkedAsReference_OnSecondOccurrence() = async { + let formatter = DependencyTreeFormatter() + let moduleA = createModel() + let moduleB = createModel() + let shared = createModel() + + moduleA.IsDependencyFor.Add(shared) + moduleB.IsDependencyFor.Add(shared) + + let tree = formatter.FormatTree([| moduleA; moduleB |]) + let output = renderToString tree + + do! check(Assert.That(output.Contains("SharedModule")).IsTrue()) + do! check(Assert.That(output.Contains("(↑)")).IsTrue()) + } + + [] + member _.FormatTree_DiamondDependency_ShowsReferenceMarkerForSharedLeaf() = async { + let formatter = DependencyTreeFormatter() + let moduleA = createModel() + let moduleB = createModel() + let moduleC = createModel() + let moduleD = createModel() + + moduleA.IsDependencyFor.Add(moduleB) + moduleA.IsDependencyFor.Add(moduleC) + moduleB.IsDependencyFor.Add(moduleD) + moduleC.IsDependencyFor.Add(moduleD) + + let tree = formatter.FormatTree([| moduleA |]) + let output = renderToString tree + + do! check(Assert.That(output.Contains("ModuleA")).IsTrue()) + do! check(Assert.That(output.Contains("ModuleB")).IsTrue()) + do! check(Assert.That(output.Contains("ModuleC")).IsTrue()) + do! check(Assert.That(output.Contains("ModuleD")).IsTrue()) + do! check(Assert.That(output.Contains("(↑)")).IsTrue()) + } + + [] + member _.FormatTree_EmptyCollection_OnlyContainsHeader() = async { + let formatter = DependencyTreeFormatter() + let roots = Array.empty + + let tree = formatter.FormatTree(roots) + let output = renderToString tree + + do! check(Assert.That(output.Contains("Module Dependencies")).IsTrue()) + do! check(Assert.That(output.Contains("├")).IsFalse()) + do! check(Assert.That(output.Contains("└")).IsFalse()) + } + + [] + member _.FormatTree_AlreadyPrintedRoot_SkipsIt() = async { + let formatter = DependencyTreeFormatter() + let moduleA = createModel() + + let tree = formatter.FormatTree([| moduleA; moduleA |]) + let output = renderToString tree + let count = output.Split([| "ModuleA" |], StringSplitOptions.None).Length - 1 + + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(count), 1)) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Engine/DisposerTests.fs b/test/ModularPipelines.UnitTests.FSharp/Engine/DisposerTests.fs new file mode 100644 index 0000000000..40e8c48d23 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Engine/DisposerTests.fs @@ -0,0 +1,45 @@ +namespace ModularPipelines.UnitTests.FSharp.Engine + +open System +open System.Threading.Tasks +open TUnit.Assertions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open TUnit.Assertions.Extensions +open ModularPipelines.Helpers + +type private AsyncDisposableClass() = + member val DisposedAsync = false with get, set + + interface IAsyncDisposable with + member this.DisposeAsync() = + this.DisposedAsync <- true + ValueTask() + +type private DisposableClass() = + member val Disposed = false with get, set + + interface IDisposable with + member this.Dispose() = + this.Disposed <- true + +type DisposerTests() = + [] + member _.Disposer_Calls_Async() = async { + let myClass = AsyncDisposableClass() + do! check(Assert.That(myClass.DisposedAsync).IsFalse()) + + do! Disposer.DisposeObjectAsync(myClass) |> Async.AwaitTask + + do! check(Assert.That(myClass.DisposedAsync).IsTrue()) + } + + [] + member _.Disposer_Calls_Sync() = async { + let myClass = new DisposableClass() + do! check(Assert.That(myClass.Disposed).IsFalse()) + + do! Disposer.DisposeObjectAsync(myClass) |> Async.AwaitTask + + do! check(Assert.That(myClass.Disposed).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Engine/MetricsCollectorTests.fs b/test/ModularPipelines.UnitTests.FSharp/Engine/MetricsCollectorTests.fs new file mode 100644 index 0000000000..010e3c6c5a --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Engine/MetricsCollectorTests.fs @@ -0,0 +1,190 @@ +namespace ModularPipelines.UnitTests.FSharp.Engine + +open System +open System.Threading +open System.Threading.Tasks +open ModularPipelines.Context +open ModularPipelines.Enums +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open TUnit.Assertions.Sources +open ModularPipelines.Models + +type private QuickModule1() = + inherit Module() + + override _.ExecuteAsync(_: IModuleContext, cancellationToken: CancellationToken) = + task { + do! Task.Delay(10, cancellationToken) + return "Done" + } + +type private QuickModule2() = + inherit Module() + + override _.ExecuteAsync(_: IModuleContext, cancellationToken: CancellationToken) = + task { + do! Task.Delay(10, cancellationToken) + return "Done" + } + +type private QuickModule3() = + inherit Module() + + override _.ExecuteAsync(_: IModuleContext, cancellationToken: CancellationToken) = + task { + do! Task.Delay(10, cancellationToken) + return "Done" + } + +[] +type MetricsCollectorTests() = + [] + member _.PipelineSummary_ContainsMetrics() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + do! check(Assert.That(result.Metrics).IsNotNull()) + } + + [] + member _.PipelineMetrics_HasParallelismFactor() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + let metrics = result.Metrics + do! check(Assert.That(metrics).IsNotNull()) + do! check(Assert.That(metrics.ParallelismFactor >= 0.0).IsTrue()) + } + + [] + member _.PipelineMetrics_HasPeakConcurrency() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + let metrics = result.Metrics + do! check(Assert.That(metrics).IsNotNull()) + do! check(Assert.That(metrics.PeakConcurrency >= 1).IsTrue()) + } + + [] + member _.PipelineMetrics_HasAverageConcurrency() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + let metrics = result.Metrics + do! check(Assert.That(metrics).IsNotNull()) + do! check(Assert.That(metrics.AverageConcurrency >= 0.0).IsTrue()) + } + + [] + member _.PipelineMetrics_HasEfficiency() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + let metrics = result.Metrics + do! check(Assert.That(metrics).IsNotNull()) + do! check(Assert.That(metrics.Efficiency >= 0.0).IsTrue()) + do! check(Assert.That(metrics.Efficiency <= 1.0).IsTrue()) + } + + [] + member _.PipelineMetrics_HasModuleCounts() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + let metrics = result.Metrics + do! check(Assert.That(metrics).IsNotNull()) + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(metrics.TotalModules), 3)) + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(metrics.SuccessfulModules), 3)) + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(metrics.FailedModules), 0)) + } + + [] + member _.PipelineMetrics_HasTimingData() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + let metrics = result.Metrics + do! check(Assert.That(metrics).IsNotNull()) + do! check(Assert.That(metrics.WallClockDuration > TimeSpan.Zero).IsTrue()) + do! check(Assert.That(metrics.TotalModuleExecutionTime >= TimeSpan.Zero).IsTrue()) + } + + [] + member _.PipelineSummary_ContainsModuleTimelines() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + do! check(Assert.That(result.ModuleTimelines).IsNotNull()) + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(result.ModuleTimelines.Count), 2)) + } + + [] + member _.ModuleTimeline_ContainsModuleName() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.ModuleTimelines).IsNotNull()) + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(result.ModuleTimelines.Count), 1)) + do! check(StringEqualsAssertionExtensions.IsEqualTo(Assert.That(result.ModuleTimelines[0].ModuleName), "QuickModule1")) + } + + [] + member _.ModuleTimeline_ContainsTimingData() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.ModuleTimelines).IsNotNull()) + + let timeline = result.ModuleTimelines[0] + do! check(Assert.That(timeline.StartTime).IsNotNull()) + do! check(Assert.That(timeline.EndTime).IsNotNull()) + do! check(Assert.That(timeline.ExecutionDuration).IsNotNull()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Execution/AlwaysRunTests.fs b/test/ModularPipelines.UnitTests.FSharp/Execution/AlwaysRunTests.fs new file mode 100644 index 0000000000..4409620830 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Execution/AlwaysRunTests.fs @@ -0,0 +1,91 @@ +namespace ModularPipelines.UnitTests.FSharp.Execution + +open System +open System.Threading +open System.Threading.Tasks +open Microsoft.Extensions.DependencyInjection +open ModularPipelines.Attributes +open ModularPipelines.Configuration +open ModularPipelines.Context +open ModularPipelines.Engine +open ModularPipelines.Extensions +open ModularPipelines.Enums +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open TUnit.Assertions.Extensions + +type private MyModule1() = + inherit ThrowingTestModule() + +[)>] +type private MyModule2() = + inherit Module() + + override _.Configure() = + ModuleConfiguration.Create().WithAlwaysRun().Build() + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + return raise (Exception()) + } + +[)>] +type private MyModule3() = + inherit Module() + + override _.Configure() = + ModuleConfiguration.Create().WithAlwaysRun().Build() + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + return raise (Exception()) + } + +[)>] +type private MyModule4() = + inherit Module() + + override _.Configure() = + ModuleConfiguration.Create().WithAlwaysRun().Build() + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + return raise (Exception()) + } + +type AlwaysRunTests() = + inherit TestBase() + + [] + member _.AlwaysRunModules_Will_Run_Even_With_Exception() = async { + let! host = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .AddModule() + .AddModule() + .BuildHostAsync() + |> Async.AwaitTask + + try + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + with _ -> + () + + let resultRegistry = host.RootServices.GetRequiredService() + let result1 = resultRegistry.GetResult(typeof) + let result2 = resultRegistry.GetResult(typeof) + let result3 = resultRegistry.GetResult(typeof) + let result4 = resultRegistry.GetResult(typeof) + + do! check(Assert.That(result1.ModuleStatus).IsEqualTo(Status.Failed)) + do! check(Assert.That(result2.ModuleStatus).IsEqualTo(Status.Failed)) + do! check(Assert.That(result3.ModuleStatus).IsEqualTo(Status.Failed)) + do! check(Assert.That(result4.ModuleStatus).IsNotEqualTo(Status.NotYetStarted)) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Execution/AsyncDisposableModuleTests.fs b/test/ModularPipelines.UnitTests.FSharp/Execution/AsyncDisposableModuleTests.fs new file mode 100644 index 0000000000..bbf53b5e62 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Execution/AsyncDisposableModuleTests.fs @@ -0,0 +1,46 @@ +namespace ModularPipelines.UnitTests.FSharp.Execution + +open System +open System.Threading +open System.Threading.Tasks +open System.Linq +open ModularPipelines.Context +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open TUnit.Assertions.Extensions + +type private AsyncDisposableModule() = + inherit Module() + + member val IsDisposed = false with get, set + + override _.ExecuteAsync(_: IModuleContext, cancellationToken: CancellationToken) = + task { + do! Task.Delay(1, cancellationToken) + return true + } + + interface IAsyncDisposable with + member this.DisposeAsync() = + ValueTask( + task { + do! Task.Delay(1) + this.IsDisposed <- true + GC.SuppressFinalize(this) + } + ) + +type AsyncDisposableModuleTests() = + [] + member _.SuccessfullyDisposed() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create().AddModule().ExecutePipelineAsync() + |> Async.AwaitTask + + let isDisposed = pipelineSummary.Modules.OfType().Single().IsDisposed + do! check(Assert.That(isDisposed).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Execution/ComposableModuleTests.fs b/test/ModularPipelines.UnitTests.FSharp/Execution/ComposableModuleTests.fs new file mode 100644 index 0000000000..a5d9a69cc5 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Execution/ComposableModuleTests.fs @@ -0,0 +1,145 @@ +namespace ModularPipelines.UnitTests.FSharp.Execution + +open System +open System.Linq +open System.Threading +open System.Threading.Tasks +open Microsoft.Extensions.DependencyInjection +open ModularPipelines.Configuration +open ModularPipelines.Context +open ModularPipelines.Engine +open ModularPipelines.Extensions +open ModularPipelines.Enums +open ModularPipelines.Modules +open ModularPipelines.Models +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open TUnit.Assertions.Extensions + +type private AlwaysSkippedModule() = + inherit Module() + + override _.Configure() = + ModuleConfiguration.Create() + .WithSkipWhen(Func(fun () -> SkipDecision.Skip("Skipped via composition"))) + .Build() + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = Task.FromResult("Executed") + +type private NeverSkippedModule() = + inherit Module() + + override _.Configure() = + ModuleConfiguration.Create() + .WithSkipWhen(Func(fun () -> SkipDecision.DoNotSkip)) + .Build() + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = Task.FromResult("Executed") + +type private TimeoutableModule() = + inherit Module() + + override _.Configure() = + ModuleConfiguration.Create().WithTimeout(TimeSpan.FromSeconds(5:int64)).Build() + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = Task.FromResult("Executed with timeout") + +type private MultiBehaviorModule() = + inherit Module() + + static let mutable beforeHookCalled = false + static let mutable afterHookCalled = false + + static member BeforeHookCalled = beforeHookCalled + static member AfterHookCalled = afterHookCalled + static member Reset() = + beforeHookCalled <- false + afterHookCalled <- false + + override _.Configure() = + ModuleConfiguration.Create() + .WithTimeout(TimeSpan.FromMinutes(1:int64)) + .WithSkipWhen(Func(fun () -> SkipDecision.DoNotSkip)) + .WithBeforeExecute(Func(fun _ -> task { beforeHookCalled <- true })) + .WithAfterExecute(Func(fun _ -> task { afterHookCalled <- true })) + .Build() + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = Task.FromResult(42) + +type private AlwaysRunModule() = + inherit Module() + + override _.Configure() = + ModuleConfiguration.Create().WithAlwaysRun().Build() + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = Task.FromResult("Always ran") + +type ComposableModuleTests() = + [] + member _.Skippable_Module_Is_Skipped_When_Condition_True() = async { + let! host = + TestPipelineHostBuilder.Create().AddModule().BuildHostAsync() + |> Async.AwaitTask + + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + + let resultRegistry = host.RootServices.GetRequiredService() + let moduleResult = resultRegistry.GetResult(typeof) + + do! check(Assert.That(moduleResult.SkipDecisionOrDefault.ShouldSkip).IsTrue()) + do! check(Assert.That(moduleResult.SkipDecisionOrDefault.Reason).IsEqualTo(SkipDecision.Skip("Skipped via composition"))) + } + + [] + member _.Skippable_Module_Executes_When_Condition_False() = async { + let! host = + TestPipelineHostBuilder.Create().AddModule().BuildHostAsync() + |> Async.AwaitTask + + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + + let resultRegistry = host.RootServices.GetRequiredService() + let moduleResult = resultRegistry.GetResult(typeof) + + do! check(Assert.That(moduleResult.SkipDecisionOrDefault.ShouldSkip).IsFalse()) + } + + [] + member _.Timeoutable_Module_Has_Custom_Timeout() = async { + let! host = + TestPipelineHostBuilder.Create().AddModule().BuildHostAsync() + |> Async.AwaitTask + + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + + let resultRegistry = host.RootServices.GetRequiredService() + let moduleResult = resultRegistry.GetResult(typeof) + + do! check(Assert.That(moduleResult.ModuleStatus).IsEqualTo(Status.Successful)) + } + + [] + member _.Multi_Behavior_Module_Calls_Hooks() = async { + MultiBehaviorModule.Reset() + + do! TestPipelineHostBuilder.Create().AddModule().ExecutePipelineAsync() + |> Async.AwaitTask + |> Async.Ignore + + do! check(Assert.That(MultiBehaviorModule.BeforeHookCalled).IsTrue()) + do! check(Assert.That(MultiBehaviorModule.AfterHookCalled).IsTrue()) + } + + [] + member _.AlwaysRun_Module_Has_Correct_Configuration() = async { + let! host = + TestPipelineHostBuilder.Create().AddModule().BuildHostAsync() + |> Async.AwaitTask + + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + + let alwaysRunModule = host.RootServices.GetServices().OfType().Single() + do! check(Assert.That(alwaysRunModule).IsNotNull()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Execution/ConcurrencyOptionsTests.fs b/test/ModularPipelines.UnitTests.FSharp/Execution/ConcurrencyOptionsTests.fs new file mode 100644 index 0000000000..47e82c301f --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Execution/ConcurrencyOptionsTests.fs @@ -0,0 +1,90 @@ +namespace ModularPipelines.UnitTests.FSharp.Execution + +open System +open System.Threading +open System.Threading.Tasks +open ModularPipelines.Context +open ModularPipelines.Enums +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.Options +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open TUnit.Assertions.Extensions + +type SimpleModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("Done") + +type SimpleModule2() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + Task.FromResult("Done") + +[] +type ConcurrencyOptionsTests() = + inherit TestBase() + + [] + member _.ConcurrencyOptions_HasCorrectDefaultValues() = async { + let options = ConcurrencyOptions() + let expectedMaxParallelism = Environment.ProcessorCount * 4 + + do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(options.MaxParallelism), expectedMaxParallelism)) + do! check(Assert.That(options.MaxCpuIntensiveModules).IsEqualTo(Environment.ProcessorCount)) + do! check(Assert.That(options.MaxIoIntensiveModules).IsNull()) + } + + [] + member _.Pipeline_RespectsMaxParallelismSetting() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ConfigurePipelineOptions(fun _ options -> + options.Concurrency.MaxParallelism <- 2 + ) + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.Pipeline_RespectsMaxCpuIntensiveModulesSetting() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .ConfigurePipelineOptions(fun _ options -> + options.Concurrency.MaxCpuIntensiveModules <- 1 + ) + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.Pipeline_RespectsMaxIoIntensiveModulesSetting() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .ConfigurePipelineOptions(fun _ options -> + options.Concurrency.MaxIoIntensiveModules <- Nullable 10 + ) + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.PipelineOptions_HasConcurrencyProperty() = async { + let options = PipelineOptions() + + do! check(Assert.That(options.Concurrency).IsNotNull()) + do! check(Assert.That(options.Concurrency).IsTypeOf()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Execution/DisposableModuleTests.fs b/test/ModularPipelines.UnitTests.FSharp/Execution/DisposableModuleTests.fs new file mode 100644 index 0000000000..5bed7344e8 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Execution/DisposableModuleTests.fs @@ -0,0 +1,41 @@ +namespace ModularPipelines.UnitTests.FSharp.Execution + +open System +open System.Threading +open System.Threading.Tasks +open System.Linq +open ModularPipelines.Context +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open TUnit.Assertions.Extensions + +type private DisposableModule() = + inherit Module() + + member val IsDisposed = false with get, set + + override _.ExecuteAsync(_: IModuleContext, cancellationToken: CancellationToken) = + task { + do! Task.Delay(1, cancellationToken) + return true + } + + interface IDisposable with + member this.Dispose() = + this.IsDisposed <- true + GC.SuppressFinalize(this) + +type DisposableModuleTests() = + [] + member _.SuccessfullyDisposed() = async { + let! pipelineSummary = + TestPipelineHostBuilder.Create().AddModule().ExecutePipelineAsync() + |> Async.AwaitTask + + let isDisposed = pipelineSummary.Modules.OfType().Single().IsDisposed + do! check(Assert.That(isDisposed).IsTrue()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Execution/EngineCancellationTokenTests.fs b/test/ModularPipelines.UnitTests.FSharp/Execution/EngineCancellationTokenTests.fs new file mode 100644 index 0000000000..7e4aa23771 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Execution/EngineCancellationTokenTests.fs @@ -0,0 +1,135 @@ +namespace ModularPipelines.UnitTests.FSharp.Execution + +open System +open System.Threading +open System.Threading.Tasks +open Microsoft.Extensions.DependencyInjection +open ModularPipelines.Attributes +open ModularPipelines.Configuration +open ModularPipelines.Context +open ModularPipelines.Engine +open ModularPipelines.Enums +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open TUnit.Assertions.Extensions +[] +module private Internal = + let waitForCancellationDelay = TimeSpan.FromMilliseconds(100:int64) + +type private BadModule() = + inherit ThrowingTestModule() + +[)>] +type private Module1CancellationTest() = + inherit SimpleTestModule() + override _.Result = true + +type private LongRunningModule() = + inherit Module() + + let taskCompletionSource = TaskCompletionSource() + + override _.ExecuteAsync(_: IModuleContext, cancellationToken: CancellationToken) = + task { + let! _ = taskCompletionSource.Task.WaitAsync(cancellationToken) + return true + } + +type private LongRunningModuleWithoutCancellation() = + inherit Module() + + let taskCompletionSource = TaskCompletionSource() + + override _.Configure() = + ModuleConfiguration.Create().WithTimeout(TimeSpan.FromSeconds(10:int64)).Build() + + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + let! _ = taskCompletionSource.Task + return true + } + +[] +type EngineCancellationTokenTests() = + inherit TestBase() + + [] + member _.When_Cancel_Engine_Token_With_DependsOn_Then_Modules_Cancel() = async { + let builder = TestPipelineHostBuilder.Create().AddModule().AddModule() + builder.Options.ThrowOnPipelineFailure <- true + + let! host = builder.BuildHostAsync() |> Async.AwaitTask + let resultRegistry = host.RootServices.GetRequiredService() + + let mutable threw = false + + try + do! host.ExecutePipelineAsync() |> Async.AwaitTask |> Async.Ignore + with _ -> + threw <- true + + let module1Result = resultRegistry.GetResult(typeof) + + do! check(Assert.That(threw).IsTrue()) + do! check(Assert.That(module1Result).IsNotNull()) + do! check(Assert.That(module1Result.ModuleStatus).IsEqualTo(Status.PipelineTerminated)) + } + + [] + member _.When_Cancel_Engine_Token_Without_DependsOn_Then_Modules_Cancel() = async { + let builder = TestPipelineHostBuilder.Create().AddModule().AddModule() + builder.Options.ThrowOnPipelineFailure <- true + + let! host = builder.BuildHostAsync() |> Async.AwaitTask + let resultRegistry = host.RootServices.GetRequiredService() + let pipelineTask = host.ExecutePipelineAsync() + + do! Task.Delay(waitForCancellationDelay) |> Async.AwaitTask + + let mutable threw = false + + try + do! pipelineTask |> Async.AwaitTask |> Async.Ignore + with _ -> + threw <- true + + let longRunningModuleResult = resultRegistry.GetResult(typeof) + + do! check(Assert.That(threw).IsTrue()) + do! check(Assert.That(longRunningModuleResult).IsNotNull()) + do! check(Assert.That(longRunningModuleResult.ModuleStatus).IsEqualTo(Status.PipelineTerminated)) + do! check(Assert.That(longRunningModuleResult.ModuleDuration).IsLessThan(TimeSpan.FromSeconds(5:int64))) + } + + [] + member _.When_Cancel_Engine_Token_Without_DependsOn_Then_Modules_Cancel_Without_Cancellation() = async { + let builder = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + + builder.Options.ThrowOnPipelineFailure <- true + + let! host = builder.BuildHostAsync() |> Async.AwaitTask + let resultRegistry = host.RootServices.GetRequiredService() + let pipelineTask = host.ExecutePipelineAsync() + + do! Task.Delay(waitForCancellationDelay) |> Async.AwaitTask + + let mutable threw = false + + try + do! pipelineTask |> Async.AwaitTask |> Async.Ignore + with _ -> + threw <- true + + let longRunningModuleResult = resultRegistry.GetResult(typeof) + + do! check(Assert.That(threw).IsTrue()) + do! check(Assert.That(longRunningModuleResult).IsNotNull()) + do! check(Assert.That(longRunningModuleResult.ModuleStatus).IsEqualTo(Status.PipelineTerminated)) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Execution/ExecutionHintTests.fs b/test/ModularPipelines.UnitTests.FSharp/Execution/ExecutionHintTests.fs new file mode 100644 index 0000000000..e3aebbaa54 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Execution/ExecutionHintTests.fs @@ -0,0 +1,162 @@ +namespace ModularPipelines.UnitTests.FSharp.Execution + +open System.Collections.Concurrent +open System.Threading +open System.Threading.Tasks +open ModularPipelines.Attributes +open ModularPipelines.Context +open ModularPipelines.Enums +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core +open TUnit.Assertions.Extensions + +module private SharedState = + let cpuModulesExecuting = ConcurrentBag() + let cpuViolations = ConcurrentBag() + let mutable maxCpuConcurrency = 0 + +[] +type private CpuIntensiveModule1() = + inherit Module() + + override this.ExecuteAsync(_: IModuleContext, cancellationToken: CancellationToken) = + task { + let moduleName = this.GetType().Name + SharedState.cpuModulesExecuting.Add(moduleName.ToString()) + + let currentCount = SharedState.cpuModulesExecuting.Count + if currentCount > SharedState.maxCpuConcurrency then + Interlocked.Exchange(&SharedState.maxCpuConcurrency, currentCount) |> ignore + + do! Task.Delay(50, cancellationToken) + + if SharedState.cpuModulesExecuting.Count > 2 then + SharedState.cpuViolations.Add($"{moduleName}: {SharedState.cpuModulesExecuting.Count} concurrent CPU-intensive modules") + + let mutable ignored = Unchecked.defaultof + SharedState.cpuModulesExecuting.TryTake(&ignored) |> ignore + return moduleName + } + +[] +type private CpuIntensiveModule2() = + inherit Module() + + override this.ExecuteAsync(_: IModuleContext, cancellationToken: CancellationToken) = + task { + let moduleName = this.GetType().Name + SharedState.cpuModulesExecuting.Add(moduleName) + + let currentCount = SharedState.cpuModulesExecuting.Count + if currentCount > SharedState.maxCpuConcurrency then + Interlocked.Exchange(&SharedState.maxCpuConcurrency, currentCount) |> ignore + + do! Task.Delay(50, cancellationToken) + + let mutable ignored = Unchecked.defaultof + SharedState.cpuModulesExecuting.TryTake(&ignored) |> ignore + return moduleName + } + +[] +type private CpuIntensiveModule3() = + inherit Module() + + override this.ExecuteAsync(_: IModuleContext, cancellationToken: CancellationToken) = + task { + let moduleName = this.GetType().Name + SharedState.cpuModulesExecuting.Add(moduleName) + + let currentCount = SharedState.cpuModulesExecuting.Count + if currentCount > SharedState.maxCpuConcurrency then + Interlocked.Exchange(&SharedState.maxCpuConcurrency, currentCount) |> ignore + + do! Task.Delay(50, cancellationToken) + + let mutable ignored = Unchecked.defaultof + SharedState.cpuModulesExecuting.TryTake(&ignored) |> ignore + return moduleName + } + +[] +type private IoIntensiveModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, cancellationToken: CancellationToken) = + task { + do! Task.Delay(10, cancellationToken) + return "IoIntensive" + } + +[] +type private DefaultExecutionTypeModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = Task.FromResult("Default") + +type private NoHintModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = Task.FromResult("NoHint") + +[] +type ExecutionHintTests() = + inherit TestBase() + + [] + member _.ClearState() = + let mutable executingItem = Unchecked.defaultof + while SharedState.cpuModulesExecuting.TryTake(&executingItem) do () + + let mutable violationItem = Unchecked.defaultof + while SharedState.cpuViolations.TryTake(&violationItem) do () + + SharedState.maxCpuConcurrency <- 0 + + [] + member _.ExecutionHintAttribute_CanBeAppliedToModule() = async { + let! result = + TestPipelineHostBuilder.Create().AddModule().ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.ModulesWithoutExecutionHint_UseDefaultType() = async { + let! result = + TestPipelineHostBuilder.Create().AddModule().ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.AllExecutionTypes_ExecuteSuccessfully() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.CpuIntensiveModules_AreThrottled() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .AddModule() + .ConfigurePipelineOptions(fun _ options -> options.Concurrency.MaxCpuIntensiveModules <- 2) + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + do! check(Assert.That(SharedState.maxCpuConcurrency).IsLessThanOrEqualTo(2)) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Execution/NotInParallelTestsWithConstraintKeys.fs b/test/ModularPipelines.UnitTests.FSharp/Execution/NotInParallelTestsWithConstraintKeys.fs new file mode 100644 index 0000000000..165180c372 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Execution/NotInParallelTestsWithConstraintKeys.fs @@ -0,0 +1,54 @@ +namespace ModularPipelines.UnitTests.FSharp.Execution + +open System.Collections.Generic +open ModularPipelines.Extensions +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module private NotInParallelTestsWithConstraintKeysData = + let tracker = NotInParallelTracker() + +[] +type ModuleWithAConstraintKey1() = + inherit NotInParallelTestModule() + override _.Tracker = NotInParallelTestsWithConstraintKeysData.tracker + override _.ConflictingModuleNames = ["ModuleWithAConstraintKey2"] :> IEnumerable + +[] +type ModuleWithAConstraintKey2() = + inherit NotInParallelTestModule() + override _.Tracker = NotInParallelTestsWithConstraintKeysData.tracker + override _.ConflictingModuleNames = ["ModuleWithAConstraintKey1"] :> IEnumerable + +[] +type ModuleWithBConstraintKey1() = + inherit NotInParallelTestModule() + override _.Tracker = NotInParallelTestsWithConstraintKeysData.tracker + override _.ConflictingModuleNames = ["ModuleWithBConstraintKey2"] :> IEnumerable + +[] +type ModuleWithBConstraintKey2() = + inherit NotInParallelTestModule() + override _.Tracker = NotInParallelTestsWithConstraintKeysData.tracker + override _.ConflictingModuleNames = ["ModuleWithBConstraintKey1"] :> IEnumerable + +type NotInParallelTestsWithConstraintKeys() = + inherit TestBase() + + [] + member _.NotInParallel_If_Same_ConstraintKey() = async { + NotInParallelTestsWithConstraintKeysData.tracker.Reset() + + let! _ = TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(NotInParallelTestsWithConstraintKeysData.tracker.Violations).IsEmpty()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Execution/NotInParallelTestsWithMultipleConstraintKeys.fs b/test/ModularPipelines.UnitTests.FSharp/Execution/NotInParallelTestsWithMultipleConstraintKeys.fs new file mode 100644 index 0000000000..27e51897e4 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Execution/NotInParallelTestsWithMultipleConstraintKeys.fs @@ -0,0 +1,54 @@ +namespace ModularPipelines.UnitTests.FSharp.Execution + +open System.Collections.Generic +open ModularPipelines.Extensions +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module private NotInParallelTestsWithMultipleConstraintKeysData = + let tracker = NotInParallelTracker() + +[] +type Module1() = + inherit NotInParallelTestModule() + override _.Tracker = NotInParallelTestsWithMultipleConstraintKeysData.tracker + override _.ConflictingModuleNames = ["Module2"] :> IEnumerable + +[] +type Module2() = + inherit NotInParallelTestModule() + override _.Tracker = NotInParallelTestsWithMultipleConstraintKeysData.tracker + override _.ConflictingModuleNames = ["Module1"; "Module3"] :> IEnumerable + +[] +type Module3() = + inherit NotInParallelTestModule() + override _.Tracker = NotInParallelTestsWithMultipleConstraintKeysData.tracker + override _.ConflictingModuleNames = ["Module2"] :> IEnumerable + +[] +type Module4() = + inherit NotInParallelTestModule() + override _.Tracker = NotInParallelTestsWithMultipleConstraintKeysData.tracker + override _.ConflictingModuleNames = [] :> IEnumerable + override _.DelayMs = 50 + +type NotInParallelTestsWithMultipleConstraintKeys() = + inherit TestBase() + + [] + member _.NotInParallel_If_Any_Modules_Executing_With_Any_Of_Same_ConstraintKey() = async { + NotInParallelTestsWithMultipleConstraintKeysData.tracker.Reset() + + let! _ = TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + do! check(Assert.That(NotInParallelTestsWithMultipleConstraintKeysData.tracker.Violations).IsEmpty()) + } diff --git a/test/ModularPipelines.UnitTests.FSharp/Extensions/FolderExtensions.fs b/test/ModularPipelines.UnitTests.FSharp/Extensions/FolderExtensions.fs new file mode 100644 index 0000000000..bc4a437770 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Extensions/FolderExtensions.fs @@ -0,0 +1,25 @@ +namespace ModularPipelines.UnitTests.FSharp.Extensions + +open System.Linq +open ModularPipelines.FileSystem + +module FolderExtensions = + let findAncestorContainingProject (original: Folder) = + let rec findProject (folder: Folder option) = + match folder with + | None -> None + | Some f -> + let hasProject = + f.ListFiles() + |> Seq.exists (fun x -> x.Extension = ".csproj" || x.Extension = ".fsproj") + if hasProject then + Some f + else + findProject (Option.ofObj f.Parent) + findProject (Some original) + +type FolderExtensions = + [] + static member FindAncestorContainingProject(original: Folder) = + FolderExtensions.findAncestorContainingProject original + |> Option.toObj diff --git a/test/ModularPipelines.UnitTests.FSharp/Helpers/SerializationTestModels.fs b/test/ModularPipelines.UnitTests.FSharp/Helpers/SerializationTestModels.fs new file mode 100644 index 0000000000..5b7d413022 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Helpers/SerializationTestModels.fs @@ -0,0 +1,45 @@ +namespace ModularPipelines.UnitTests.FSharp.Helpers + +open System.Collections.Generic +open System.Text.Json.Serialization + +/// +/// Shared test models for serialization tests (JSON, XML, YAML). +/// Reduces duplication of similar model definitions across test files. +/// +module SerializationTestModels = + /// + /// Standard test values used across serialization tests. + /// + module TestValues = + let FooValue = "Bar!" + let HelloValue = "World!" + let ItemsValue = [| "One"; "Two"; "3" |] + + /// + /// Base test model with common properties for serialization testing. + /// + [] + type SerializationTestModel = + { + Foo: string + Hello: string + [] + Items: List + } + + /// + /// Creates a new model with default test values. + /// + static member CreateDefault() = + { Foo = TestValues.FooValue + Hello = TestValues.HelloValue + Items = null } + + /// + /// Creates a new model with default test values including items. + /// + static member CreateWithItems() = + { Foo = TestValues.FooValue + Hello = TestValues.HelloValue + Items = List(TestValues.ItemsValue) } diff --git a/test/ModularPipelines.UnitTests.FSharp/Logging/StringLogger.fs b/test/ModularPipelines.UnitTests.FSharp/Logging/StringLogger.fs new file mode 100644 index 0000000000..2420483a39 --- /dev/null +++ b/test/ModularPipelines.UnitTests.FSharp/Logging/StringLogger.fs @@ -0,0 +1,17 @@ +namespace ModularPipelines.UnitTests.FSharp.Logging + +open System +open System.Text +open Microsoft.Extensions.Logging + +type StringLogger<'T>(stringBuilder: StringBuilder) = + interface ILogger<'T> with + member _.Log<'TState>(logLevel: LogLevel, eventId: EventId, state: 'TState, + ex: Exception, formatter: Func<'TState, Exception, string>) = + let log = formatter.Invoke(state, ex) + stringBuilder.AppendLine(log) |> ignore + + member _.IsEnabled(logLevel: LogLevel) = true + + member _.BeginScope<'TState when 'TState: not null>(_: 'TState) : IDisposable = + { new IDisposable with member _.Dispose() = () } diff --git a/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj b/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj index ec6badbd47..e3a4e5cbcb 100644 --- a/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj +++ b/test/ModularPipelines.UnitTests.FSharp/ModularPipelines.UnitTests.FSharp.fsproj @@ -32,7 +32,23 @@ + + + + + + + + + + + + + + + + diff --git a/test/ModularPipelines.UnitTests/Extensions/FolderExtensions.cs b/test/ModularPipelines.UnitTests/Extensions/FolderExtensions.cs index 47a0821d4d..e984bb306f 100644 --- a/test/ModularPipelines.UnitTests/Extensions/FolderExtensions.cs +++ b/test/ModularPipelines.UnitTests/Extensions/FolderExtensions.cs @@ -10,7 +10,7 @@ public static class FolderExtensions while (folder != null) { - if (folder.ListFiles().Any(x => x.Extension == ".csproj")) + if (folder.ListFiles().Any(x => x.Extension is ".csproj" or ".fsproj")) { return folder; } @@ -20,4 +20,4 @@ public static class FolderExtensions return null; } -} \ No newline at end of file +} From 68cbe54b24a5253d0043480131c4584fd174739c Mon Sep 17 00:00:00 2001 From: James Alickolli Date: Thu, 14 May 2026 19:32:43 +1000 Subject: [PATCH 13/14] Update FlexibleDependencyIntegrationTests.fs --- .../FlexibleDependencyIntegrationTests.fs | 1100 ++++++++--------- 1 file changed, 544 insertions(+), 556 deletions(-) diff --git a/test/ModularPipelines.UnitTests.FSharp/Dependencies/FlexibleDependencyIntegrationTests.fs b/test/ModularPipelines.UnitTests.FSharp/Dependencies/FlexibleDependencyIntegrationTests.fs index 2fe92058d5..9307a29829 100644 --- a/test/ModularPipelines.UnitTests.FSharp/Dependencies/FlexibleDependencyIntegrationTests.fs +++ b/test/ModularPipelines.UnitTests.FSharp/Dependencies/FlexibleDependencyIntegrationTests.fs @@ -1,558 +1,546 @@ namespace ModularPipelines.UnitTests.FSharp.Dependencies -//open System -//open System.Collections.Concurrent -//open System.Collections.Generic -//open System.Threading -//open System.Threading.Tasks -//open Microsoft.Extensions.DependencyInjection -//open ModularPipelines.Attributes -//open ModularPipelines.Context -//open ModularPipelines.Enums -//open ModularPipelines.Extensions -//open ModularPipelines.Modules -//open ModularPipelines.TestHelpers -//open TUnit.Assertions -//open TUnit.Assertions.Extensions -//open TUnit.Assertions.FSharp.Operations -//open TUnit.Core - -//module FlexibleDependencyIntegrationTestsData = -// let executionOrderQueue = ConcurrentQueue() - -// let clearExecutionOrder() = executionOrderQueue.Clear() - -// let getExecutionOrder() = executionOrderQueue.ToArray() |> Array.toList - -// let recordExecution moduleName = executionOrderQueue.Enqueue(moduleName) - -// [] -// type CriticalAttribute() = -// inherit Attribute() - -// [] -// type DatabaseModuleA() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof DatabaseModuleA) -// return "DatabaseA" -// } - -// [] -// type DatabaseModuleB() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof DatabaseModuleB) -// return "DatabaseB" -// } - -// [] -// type NonDatabaseModule() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof NonDatabaseModule) -// return "NonDatabase" -// } - -// [] -// type AfterDatabaseModule() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof AfterDatabaseModule) -// return "AfterDatabase" -// } - -// [] -// type ModuleDependingOnNonExistentTag() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof ModuleDependingOnNonExistentTag) -// return "DependsOnNonExistent" -// } - -// [] -// [] -// [] -// type ModuleWithMultipleTags() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof ModuleWithMultipleTags) -// return "MultipleTags" -// } - -// [] -// type AfterSlowModule() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof AfterSlowModule) -// return "AfterSlow" -// } - -// [] -// type InfrastructureModuleA() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof InfrastructureModuleA) -// return "InfrastructureA" -// } - -// [] -// type InfrastructureModuleB() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof InfrastructureModuleB) -// return "InfrastructureB" -// } - -// [] -// type BuildModule() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof BuildModule) -// return "Build" -// } - -// [] -// type AfterInfrastructureModule() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof AfterInfrastructureModule) -// return "AfterInfrastructure" -// } - -// [] -// type ModuleDependingOnNonExistentCategory() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof ModuleDependingOnNonExistentCategory) -// return "DependsOnNonExistentCategory" -// } - -// [] -// type CriticalModuleA() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof CriticalModuleA) -// return "CriticalA" -// } - -// [] -// type CriticalModuleB() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof CriticalModuleB) -// return "CriticalB" -// } - -// type NonCriticalModule() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof NonCriticalModule) -// return "NonCritical" -// } - -// [] -// type BaseCriticalModule() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof BaseCriticalModule) -// return "BaseCritical" -// } - -// type DerivedCriticalModule() = -// inherit BaseCriticalModule() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof DerivedCriticalModule) -// return "DerivedCritical" -// } - -// [>] -// type AfterCriticalModule() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof AfterCriticalModule) -// return "AfterCritical" -// } - -// type ModuleWithOverrideTags() = -// inherit Module() -// override _.Tags = HashSet([ "database"; "override-tag" ]) :> IReadOnlySet -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof ModuleWithOverrideTags) -// return "OverrideTags" -// } - -// type ModuleWithOverrideCategory() = -// inherit Module() -// override _.Category = "infrastructure" -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof ModuleWithOverrideCategory) -// return "OverrideCategory" -// } - -// type PlainModule() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof PlainModule) -// return "Plain" -// } - -// [] -// [] -// [] -// type ModuleWithMultipleFlexibleDependencies() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof ModuleWithMultipleFlexibleDependencies) -// return "MultipleFlexibleDeps" -// } - -// [] -// [] -// [] -// type AfterDatabaseModuleWithPhase1Tag() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof AfterDatabaseModuleWithPhase1Tag) -// return "AfterDbWithPhase1" -// } - -// [] -// type AfterPhase1Module() = -// inherit Module() -// override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = -// task { -// do! Task.Yield() -// recordExecution (nameof AfterPhase1Module) -// return "AfterPhase1" -// } - -//[] -//type FlexibleDependencyIntegrationTests() = -// inherit TestBase() - -// [] -// member _.Setup() = -// FlexibleDependencyIntegrationTestsData.clearExecutionOrder() - -// [] -// member _.DependsOnModulesWithTag_WaitsForTaggedModules() = async { -// let! result = -// TestPipelineHostBuilder.Create() -// .AddModule() -// .AddModule() -// .AddModule() -// .AddModule() -// .ExecutePipelineAsync() -// |> Async.AwaitTask - -// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) - -// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() -// let afterDbIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterDatabaseModule) -// let dbAIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.DatabaseModuleA) -// let dbBIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.DatabaseModuleB) - -// do! check(Assert.That(afterDbIndex).IsGreaterThan(dbAIndex)) -// do! check(Assert.That(afterDbIndex).IsGreaterThan(dbBIndex)) -// } - -// [] -// member _.DependsOnModulesWithTag_NoMatchingModules_StillSucceeds() = async { -// let! result = -// TestPipelineHostBuilder.Create() -// .AddModule() -// .AddModule() -// .ExecutePipelineAsync() -// |> Async.AwaitTask - -// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) -// } - -// [] -// member _.DependsOnModulesWithTag_MultipleTagsOnModule_MatchesCorrectly() = async { -// let! result = -// TestPipelineHostBuilder.Create() -// .AddModule() -// .AddModule() -// .ExecutePipelineAsync() -// |> Async.AwaitTask - -// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) - -// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() -// let multiTagIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.ModuleWithMultipleTags) -// let afterSlowIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterSlowModule) - -// do! check(Assert.That(afterSlowIndex).IsGreaterThan(multiTagIndex)) -// } - -// [] -// member _.DependsOnModulesInCategory_WaitsForCategorizedModules() = async { -// let! result = -// TestPipelineHostBuilder.Create() -// .AddModule() -// .AddModule() -// .AddModule() -// .AddModule() -// .ExecutePipelineAsync() -// |> Async.AwaitTask - -// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) - -// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() -// let afterInfraIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterInfrastructureModule) -// let infraAIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.InfrastructureModuleA) -// let infraBIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.InfrastructureModuleB) - -// do! check(Assert.That(afterInfraIndex).IsGreaterThan(infraAIndex)) -// do! check(Assert.That(afterInfraIndex).IsGreaterThan(infraBIndex)) -// } - -// [] -// member _.DependsOnModulesInCategory_NoMatchingModules_StillSucceeds() = async { -// let! result = -// TestPipelineHostBuilder.Create() -// .AddModule() -// .AddModule() -// .ExecutePipelineAsync() -// |> Async.AwaitTask - -// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) -// } - -// [] -// member _.DependsOnModulesWithAttribute_WaitsForAttributedModules() = async { -// let! result = -// TestPipelineHostBuilder.Create() -// .AddModule() -// .AddModule() -// .AddModule() -// .AddModule() -// .ExecutePipelineAsync() -// |> Async.AwaitTask - -// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) - -// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() -// let afterCriticalIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterCriticalModule) -// let criticalAIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.CriticalModuleA) -// let criticalBIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.CriticalModuleB) - -// do! check(Assert.That(afterCriticalIndex).IsGreaterThan(criticalAIndex)) -// do! check(Assert.That(afterCriticalIndex).IsGreaterThan(criticalBIndex)) -// } - -// [] -// member _.DependsOnModulesWithAttribute_InheritedAttribute_IsRecognized() = async { -// let! result = -// TestPipelineHostBuilder.Create() -// .AddModule() -// .AddModule() -// .ExecutePipelineAsync() -// |> Async.AwaitTask - -// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) - -// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() -// let derivedIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.DerivedCriticalModule) -// let afterCriticalIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterCriticalModule) - -// do! check(Assert.That(afterCriticalIndex).IsGreaterThan(derivedIndex)) -// } - -// [] -// member _.DependsOnModulesWithAttribute_NoMatchingModules_StillSucceeds() = async { -// let! result = -// TestPipelineHostBuilder.Create() -// .AddModule() -// .AddModule() -// .ExecutePipelineAsync() -// |> Async.AwaitTask - -// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) -// } - -// [] -// member _.ModuleWithOverrideTags_IsRecognizedByTagDependency() = async { -// let! result = -// TestPipelineHostBuilder.Create() -// .AddModule() -// .AddModule() -// .ExecutePipelineAsync() -// |> Async.AwaitTask - -// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) - -// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() -// let overrideIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.ModuleWithOverrideTags) -// let afterDbIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterDatabaseModule) - -// do! check(Assert.That(afterDbIndex).IsGreaterThan(overrideIndex)) -// } - -// [] -// member _.ModuleWithOverrideCategory_IsRecognizedByCategoryDependency() = async { -// let! result = -// TestPipelineHostBuilder.Create() -// .AddModule() -// .AddModule() -// .ExecutePipelineAsync() -// |> Async.AwaitTask - -// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) - -// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() -// let overrideIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.ModuleWithOverrideCategory) -// let afterInfraIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterInfrastructureModule) - -// do! check(Assert.That(afterInfraIndex).IsGreaterThan(overrideIndex)) -// } - -// [] -// member _.ModuleWithRegistrationTags_IsRecognizedByTagDependency() = async { -// let! result = -// TestPipelineHostBuilder.Create() -// .ConfigureServices(fun _ services -> -// services.AddModule().WithTags("database") |> ignore) -// .AddModule() -// .ExecutePipelineAsync() -// |> Async.AwaitTask - -// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) - -// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() -// let plainIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.PlainModule) -// let afterDbIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterDatabaseModule) - -// do! check(Assert.That(afterDbIndex).IsGreaterThan(plainIndex)) -// } - -// [] -// member _.ModuleWithRegistrationCategory_IsRecognizedByCategoryDependency() = async { -// let! result = -// TestPipelineHostBuilder.Create() -// .ConfigureServices(fun _ services -> -// services.AddModule().WithCategory("infrastructure") |> ignore) -// .AddModule() -// .ExecutePipelineAsync() -// |> Async.AwaitTask - -// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) - -// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() -// let plainIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.PlainModule) -// let afterInfraIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterInfrastructureModule) - -// do! check(Assert.That(afterInfraIndex).IsGreaterThan(plainIndex)) -// } - -// [] -// member _.ModuleWithBothAttributeAndRegistrationTags_MergesTags() = async { -// let! result = -// TestPipelineHostBuilder.Create() -// .ConfigureServices(fun _ services -> -// services.AddModule().WithTags("slow") |> ignore) -// .AddModule> -// .ExecutePipelineAsync() -// |> Async.AwaitTask - -// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) - -// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() -// let dbAIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.DatabaseModuleA) -// let afterSlowIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterSlowModule) - -// do! check(Assert.That(afterSlowIndex).IsGreaterThan(dbAIndex)) -// } - -// [] -// member _.CombinedDependencies_ModuleWithMultipleFlexibleDependencies() = async { -// let! result = -// TestPipelineHostBuilder.Create() -// .AddModule() -// .AddModule() -// .AddModule() -// .AddModule() -// .ExecutePipelineAsync() -// |> Async.AwaitTask - -// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) - -// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() -// let combinedIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.ModuleWithMultipleFlexibleDependencies) -// let dbAIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.DatabaseModuleA) -// let infraAIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.InfrastructureModuleA) -// let criticalAIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.CriticalModuleA) - -// do! check(Assert.That(combinedIndex).IsGreaterThan(dbAIndex)) -// do! check(Assert.That(combinedIndex).IsGreaterThan(infraAIndex)) -// do! check(Assert.That(combinedIndex).IsGreaterThan(criticalAIndex)) -// } - -// [] -// member _.ChainedFlexibleDependencies_ExecuteInCorrectOrder() = async { -// let! result = -// TestPipelineHostBuilder.Create() -// .AddModule() -// .AddModule() -// .AddModule() -// .ExecutePipelineAsync() -// |> Async.AwaitTask - -// do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) - -// let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() -// let dbAIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.DatabaseModuleA) -// let afterDbIndex = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterDatabaseModuleWithPhase1Tag) -// let afterPhase1Index = order.IndexOf(nameof FlexibleDependencyIntegrationTestsData.AfterPhase1Module) - -// do! check(Assert.That(afterDbIndex).IsGreaterThan(dbAIndex)) -// do! check(Assert.That(afterPhase1Index).IsGreaterThan(afterDbIndex)) -// } +open System +open System.Collections.Concurrent +open System.Collections.Generic +open System.Threading +open System.Threading.Tasks +open Microsoft.Extensions.DependencyInjection +open ModularPipelines.Attributes +open ModularPipelines.Context +open ModularPipelines.Enums +open ModularPipelines.Extensions +open ModularPipelines.Modules +open ModularPipelines.TestHelpers +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +module FlexibleDependencyIntegrationTestsData = + let executionOrderQueue = ConcurrentQueue() + + let clearExecutionOrder() = executionOrderQueue.Clear() + + let getExecutionOrder() = ResizeArray(executionOrderQueue.ToArray()) + + let recordExecution moduleName = executionOrderQueue.Enqueue(moduleName) + + [] + type CriticalAttribute() = + inherit Attribute() + + [] + type DatabaseModuleA() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof DatabaseModuleA) + return "DatabaseA" + } + + [] + type DatabaseModuleB() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof DatabaseModuleB) + return "DatabaseB" + } + + [] + type NonDatabaseModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof NonDatabaseModule) + return "NonDatabase" + } + + [] + type AfterDatabaseModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof AfterDatabaseModule) + return "AfterDatabase" + } + + [] + type ModuleDependingOnNonExistentTag() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof ModuleDependingOnNonExistentTag) + return "DependsOnNonExistent" + } + + [] + [] + [] + type ModuleWithMultipleTags() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof ModuleWithMultipleTags) + return "MultipleTags" + } + + [] + type AfterSlowModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof AfterSlowModule) + return "AfterSlow" + } + + [] + type InfrastructureModuleA() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof InfrastructureModuleA) + return "InfrastructureA" + } + + [] + type InfrastructureModuleB() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof InfrastructureModuleB) + return "InfrastructureB" + } + + [] + type BuildModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof BuildModule) + return "Build" + } + + [] + type AfterInfrastructureModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof AfterInfrastructureModule) + return "AfterInfrastructure" + } + + [] + type ModuleDependingOnNonExistentCategory() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof ModuleDependingOnNonExistentCategory) + return "DependsOnNonExistentCategory" + } + + [] + type CriticalModuleA() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof CriticalModuleA) + return "CriticalA" + } + + [] + type CriticalModuleB() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof CriticalModuleB) + return "CriticalB" + } + + type NonCriticalModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof NonCriticalModule) + return "NonCritical" + } + + [] + type BaseCriticalModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof BaseCriticalModule) + return "BaseCritical" + } + + type DerivedCriticalModule() = + inherit BaseCriticalModule() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof DerivedCriticalModule) + return "DerivedCritical" + } + + //[>] + //type AfterCriticalModule() = + // inherit Module() + // override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + // task { + // do! Task.Yield() + // recordExecution (nameof AfterCriticalModule) + // return "AfterCritical" + // } + + type ModuleWithOverrideTags() = + inherit Module() + override _.Tags = HashSet([ "database"; "override-tag" ]) :> IReadOnlySet + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof ModuleWithOverrideTags) + return "OverrideTags" + } + + type ModuleWithOverrideCategory() = + inherit Module() + override _.Category = "infrastructure" + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof ModuleWithOverrideCategory) + return "OverrideCategory" + } + + type PlainModule() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof PlainModule) + return "Plain" + } + + //[] + //[] + //[>] + //type ModuleWithMultipleFlexibleDependencies() = + // inherit Module() + // override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + // task { + // do! Task.Yield() + // recordExecution (nameof ModuleWithMultipleFlexibleDependencies) + // return "MultipleFlexibleDeps" + // } + + [] + [] + [] + type AfterDatabaseModuleWithPhase1Tag() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof AfterDatabaseModuleWithPhase1Tag) + return "AfterDbWithPhase1" + } + + [] + type AfterPhase1Module() = + inherit Module() + override _.ExecuteAsync(_: IModuleContext, _: CancellationToken) = + task { + do! Task.Yield() + recordExecution (nameof AfterPhase1Module) + return "AfterPhase1" + } + +[] +type FlexibleDependencyIntegrationTests() = + inherit TestBase() + + [] + member _.Setup() = + FlexibleDependencyIntegrationTestsData.clearExecutionOrder() + () + + [] + member _.DependsOnModulesWithTag_WaitsForTaggedModules() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + + let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() |> Seq.toArray + let afterDbIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.AfterDatabaseModule) + let dbAIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.DatabaseModuleA) + let dbBIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.DatabaseModuleB) + + do! check(Assert.That(afterDbIndex).IsGreaterThan(dbAIndex)) + do! check(Assert.That(afterDbIndex).IsGreaterThan(dbBIndex)) + } + + [] + member _.DependsOnModulesWithTag_NoMatchingModules_StillSucceeds() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + } + + [] + member _.DependsOnModulesWithTag_MultipleTagsOnModule_MatchesCorrectly() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + + let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() |> Seq.toArray + let multiTagIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.ModuleWithMultipleTags) + let afterSlowIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.AfterSlowModule) + + do! check(Assert.That(afterSlowIndex).IsGreaterThan(multiTagIndex)) + } + + [] + member _.DependsOnModulesInCategory_WaitsForCategorizedModules() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + + let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() |> Seq.toArray + let afterInfraIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.AfterInfrastructureModule) + let infraAIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.InfrastructureModuleA) + let infraBIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.InfrastructureModuleB) + + do! check(Assert.That(afterInfraIndex).IsGreaterThan(infraAIndex)) + do! check(Assert.That(afterInfraIndex).IsGreaterThan(infraBIndex)) + } + + [] + member _.DependsOnModulesInCategory_NoMatchingModules_StillSucceeds() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + } + + //[] + //member _.DependsOnModulesWithAttribute_WaitsForAttributedModules() = async { + // let! result = + // TestPipelineHostBuilder.Create() + // .AddModule() + // .AddModule() + // .AddModule() + // .AddModule() + // .ExecutePipelineAsync() + // |> Async.AwaitTask + + // do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + + // let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() |> Seq.toArray + // let afterCriticalIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.AfterCriticalModule) + // let criticalAIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.CriticalModuleA) + // let criticalBIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.CriticalModuleB) + + // do! check(Assert.That(afterCriticalIndex).IsGreaterThan(criticalAIndex)) + // do! check(Assert.That(afterCriticalIndex).IsGreaterThan(criticalBIndex)) + //} + + //[] + //member _.DependsOnModulesWithAttribute_InheritedAttribute_IsRecognized() = async { + // let! result = + // TestPipelineHostBuilder.Create() + // .AddModule() + // .AddModule() + // .ExecutePipelineAsync() + // |> Async.AwaitTask + + // do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + + // let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() |> Seq.toArray + // let derivedIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.DerivedCriticalModule) + // let afterCriticalIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.AfterCriticalModule) + + // do! check(Assert.That(afterCriticalIndex).IsGreaterThan(derivedIndex)) + //} + + [] + member _.ModuleWithOverrideTags_IsRecognizedByTagDependency() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + + let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() |> Seq.toArray + let overrideIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.ModuleWithOverrideTags) + let afterDbIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.AfterDatabaseModule) + + do! check(Assert.That(afterDbIndex).IsGreaterThan(overrideIndex)) + } + + [] + member _.ModuleWithOverrideCategory_IsRecognizedByCategoryDependency() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + + let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() |> Seq.toArray + let overrideIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.ModuleWithOverrideCategory) + let afterInfraIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.AfterInfrastructureModule) + + do! check(Assert.That(afterInfraIndex).IsGreaterThan(overrideIndex)) + } + + [] + member _.ModuleWithRegistrationTags_IsRecognizedByTagDependency() = async { + let! result = + TestPipelineHostBuilder.Create() + .ConfigureServices(fun _ services -> + services.AddModule().WithTags("database") |> ignore) + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + + let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() |> Seq.toArray + let plainIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.PlainModule) + let afterDbIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.AfterDatabaseModule) + + do! check(Assert.That(afterDbIndex).IsGreaterThan(plainIndex)) + } + + [] + member _.ModuleWithRegistrationCategory_IsRecognizedByCategoryDependency() = async { + let! result = + TestPipelineHostBuilder.Create() + .ConfigureServices(fun _ services -> + services.AddModule().WithCategory("infrastructure") |> ignore) + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + + let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() |> Seq.toArray + let plainIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.PlainModule) + let afterInfraIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.AfterInfrastructureModule) + + do! check(Assert.That(afterInfraIndex).IsGreaterThan(plainIndex)) + } + + [] + member _.ModuleWithBothAttributeAndRegistrationTags_MergesTags() = async { + let! result = + TestPipelineHostBuilder.Create() + .ConfigureServices(fun _ services -> + services.AddModule().WithTags("slow") |> ignore) + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + + let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() |> Seq.toArray + let dbAIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.DatabaseModuleA) + let afterSlowIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.AfterSlowModule) + do! check(Assert.That(afterSlowIndex).IsGreaterThan(dbAIndex)) + } + + //[] + //member _.CombinedDependencies_ModuleWithMultipleFlexibleDependencies() = async { + // let! result = + // TestPipelineHostBuilder.Create() + // .AddModule() + // .AddModule() + // .AddModule() + // .AddModule() + // .ExecutePipelineAsync() + // |> Async.AwaitTask + + // do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + + // let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() |> Seq.toArray + // let combinedIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.ModuleWithMultipleFlexibleDependencies) + // let dbAIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.DatabaseModuleA) + // let infraAIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.InfrastructureModuleA) + // let criticalAIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.CriticalModuleA) + + // do! check(Assert.That(combinedIndex).IsGreaterThan(dbAIndex)) + // do! check(Assert.That(combinedIndex).IsGreaterThan(infraAIndex)) + // do! check(Assert.That(combinedIndex).IsGreaterThan(criticalAIndex)) + //} + + [] + member _.ChainedFlexibleDependencies_ExecuteInCorrectOrder() = async { + let! result = + TestPipelineHostBuilder.Create() + .AddModule() + .AddModule() + .AddModule() + .ExecutePipelineAsync() + |> Async.AwaitTask + + do! check(Assert.That(result.Status).IsEqualTo(Status.Successful)) + + let order = FlexibleDependencyIntegrationTestsData.getExecutionOrder() |> Seq.toArray + let dbAIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.DatabaseModuleA) + let afterDbIndex = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.AfterDatabaseModuleWithPhase1Tag) + let afterPhase1Index = Array.IndexOf(order, nameof FlexibleDependencyIntegrationTestsData.AfterPhase1Module) + + do! check(Assert.That(afterDbIndex).IsGreaterThan(dbAIndex)) + do! check(Assert.That(afterPhase1Index).IsGreaterThan(afterDbIndex)) + } From 2b918a6cec8dc4a7ed69b755f0ddfde35d0a9cca Mon Sep 17 00:00:00 2001 From: James Alickolli Date: Fri, 15 May 2026 09:56:20 +1000 Subject: [PATCH 14/14] Update Directory.Packages.props --- Directory.Packages.props | 1 + 1 file changed, 1 insertion(+) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2c421fa803..1ad216e6e2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -84,6 +84,7 @@ +