├── .gitignore ├── .travis.yml ├── .vscode └── launch.json ├── README.md ├── build ├── build-context.csx ├── build.csx └── dotnet-steps.nuspec ├── main.csx ├── omnisharp.json └── src ├── async.steps.tests.csx ├── custom.summary.tests.csx ├── default.tests.csx ├── duration.tests.csx ├── singlestep.tests.csx ├── steps.csx └── steps.tests.csx /.gitignore: -------------------------------------------------------------------------------- 1 | build/tmp 2 | build/Artifacts -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | mono: latest 3 | dotnet: 2.1.500 4 | env: 5 | global: 6 | - DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true 7 | - DOTNET_CLI_TELEMETRY_OPTOUT=true 8 | - secure: gJ1OXDobmWFvWO4CJHfP3NFiPLi9S287PW5TQ2zTtZlcXYzR/Fk2RsnjQSOiXPVFrO45NnXavNS4QQvaOJiBxGTygain7NlH5k0+BQDCCIR0Jv900zi5MfQY1sSAo11S7aDy3YRdEK81VrxMBhonuZVf8c+vqL0Tcvv719cUcVQrXf388JUyax7csu1CsMm/G0vO62hF8Z7xtoqjSDuw65QN4A8EuoxaMQMux3kxJU6p0l/mmyrwGyxf9T+MA2mT04Ap3bX3Wrj22XMWUo1gN9eUX2xi1TNW/nn/2Sju4w+sR7XtesmfQpQO707muVct4JSf7TWrZcp/spSqJ1osPTamJILmE/kwbccUupnzUUtpSapvqf/+FI7jKgBa71E/dc1gIHEXSztNktvILU7xNiNbLfyfqvHkGQsU85+iXHYpPCzmwRLI+yvAJPDKviO7w58v+jV9aovHVPLV8OGnBk0vYAVXn/mlyJNxkR00N0fbP4JUElj8U477ON/s4xrjxA/1XuiJKnJCLGjdap5NDgBlkbtbfPzMu4srb28uahz9jfQSeQGwhe7SaGn3XKzFcdRCOvYpkwejZLxOZVV1ZtppnG+LpiJybzgZe6S3n4Z4JC6kInm/dHEhtcF3iDphf1YDj7GCyCtyGPua6zFtXpt0GmPNse9JWXnYpwQqhHw= 9 | - secure: Eab9j54Za803E4I2XSmViC/Uunb9EwqjSbR18dSKSgOSSbkTUJ579C+ffv2dYXwy5oMj7jPFwI1LarKvnKG+my69lNNSULW8/xORMWFJJDaKYEsYFYLMAowe1hsuuYbBI9Z72mDioMoSImzabcJnlS348iwM0IaRH1i7q8FWMf4jyQ7ZhFPOISl4pXOKjx3CEY/qLQY6TzwxmYAvrHuxqx6Vt/cGOZpwtJP6669VzyrKyrFXrVmJA0DxHsSYDedv6FQF9HrI3jssdNm2qWke4DxqY1pFjEX/48d6m/FZZccW/weII7epByY7p6T/AA7GFZ5MBKghynjsI1i6apbaZUiu0WHDHjB6Y7PvD+PenuNYFUEb20hbJbcuIMTy8HNRTZiaqMTR4UDdKSaGOtfKVk29znP81u40XvRohISVKcp/riE6d3dQYDvzIbhequA611xc5veY0l9gP3wXYsGvnMuGg5MnhcznjMgPgt+oPM8Zd6RaNmOKEnOv5mHK8wF5CzL30qiI9GEe6YHfTS4EhIH5z+syKhVzIQxYNIwBlEbZyVfKXm/Uj5lVNZ+70P/fsauPX8Mrxoe7EDUIykxX6s0gfQ1PbwdfC1P6G6Nh46xOJUhAbaq/wf8aXEdbSIc7PECThXIsJ5j+kuknJ5FSlHhDPWugFtQ1aAtPQROzSgQ= 10 | - secure: e2sE8Y+3t/bOp6rkHNZxBswD2TmmU3Dc05joyapTY6DSkRh/q0igcVtpW6ygSyyCgGRnx7dYP5x/PNK4CioPDD6m8TqwMSktOMDb2P8QVyFVt4//5y8cxujcXTR0tnmpa6kYF3gNue3BamJx7S1NqaU4Vymppxh8ryUENxxRgdBFfW6hDbOEVgyhu5G7/BJWjkFfEKnV5xglafrbsXwdmTcUfaS1ouNDQxiV73YCVfTDZaGu6r0AgKrIgLf5gRh6+74c7E/Uo+LuMdjPfjAeYpjf8yuaV7+pADFqJmuHbVGDCHXwO6mmihNqwdYVq3R1fZfCoE4/s3IV1g1SwCa4h2VcuSIJfJnnzYwkYpVQcRBNHgk014ATsARyeePaT+D/cxBMGKPzxfUAo3Hzlt/frVo5m8xP49eX1jt6TVBp0rUzW1h6iBkWWsQVpJ9tYZB0xX+kBhWtoOht8ZArTB2yorEEbYUrA+YKXBYrbwPa776NQ33aaE0QOZMI+fbS+BnOQ3399Z906IrXTCbvxJJ0D1sY6oD26T/GA/fz6AzqlW04LKO0dzkZCunXsU+l6iQahTvDEH4YRzbvRZMU0qGUN9K8FunWNzyiBwZxXbppFtSFfXa4f07fWY6JXI+RLZCoYixTv4ijPbmlGxl/wK3jZwXocM3bEp3duY1vSCDNUL4= 11 | - secure: VTDO81xdPMncFqDVcrvpbAsbswZnE7LfNamLzbN6QSMRN03RLoqkHzm1oHf2i9D2YpTHKAL/RZfkw5j3FqJRd/y9NlV4oe4AI0A4UQLga+WR5xKoJYRBZGV6MMInGPVphu6daZkT7GMUyS+riZ4lvtXybdNezA5zEVKXJLREMMru/UhzmCJjvUl2LjDvlnJ5G0WbuI+lDUu+kYAssznqJXupjpafcoMK/hESuOL1O48IG0sguG6GhvyNTFHDXvpL+/2nGuVijkG8TUpQwXyd0ujuWLhhdfwJGNTj4XJqcpxoecjnLP3+sHnbgsraWt7QWY5Qo8IqSrnni7wU8kyqDP2bvwnsOWTYbg9lmwGYgNH3i2DevRQ20Cl17MTDB6NsZWR6ynb93nCCfWeO4zagIz1O20zyt+l/bM62D+Giki6Xp2Hk0JhN51xDVfxSJ4aoPO9t4bYrhEwYmND+l/iTWi2WuAGDu/pvGQ8aBc7OISUfdN3dEVXxkAYaTU6B5cTslJVGPOG8GFMKK/l3Od6BegxhIeu5JZd7nLCyGgBd5xKmwq5Jv1bd6jqpsdA2KD9zBHFqSyYm1taakEC+vv8JcQsGRA1vMAmm81Nhy1DPuiOaxp3MPD31+7qgzmhKknwMnQ55lz9UpXS5NT94QfnVqacmINvoU59JVRBTiDMZmno= 12 | matrix: 13 | include: 14 | - os: linux 15 | dist: trusty 16 | before_script: 17 | - dotnet tool install dotnet-script -g 18 | - export PATH=$PATH:/home/travis/.dotnet/tools 19 | script: 20 | - dotnet script build/build.csx 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": ".NET Script Debug", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "program": "${env:HOME}/.dotnet/tools/dotnet-script", 9 | "args": ["${file}"], 10 | "windows": { 11 | "program": "${env:USERPROFILE}/.dotnet/tools/dotnet-script.exe", 12 | }, 13 | "cwd": "${workspaceFolder}", 14 | "stopAtEntry": true, 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotnet-steps 2 | 3 | A small step for man kind, one giant leap for build scripts. 4 | 5 | ## What is it? 6 | 7 | A super simple way of composing "steps" in a C# script. No strings attached (pun intended 😀). 8 | 9 | ## Installing 10 | 11 | Simply grab the `steps.csx`file from this repo and we are good to go. 12 | 13 | If you are on `dotnet-script` we also have a script package 14 | 15 | ```c# 16 | #load "nuget: dotnet-steps, [version]" 17 | ``` 18 | 19 | 20 | 21 | ## Getting started 22 | 23 | ```c# 24 | #load "steps.csx" 25 | 26 | Step step1 = () => WriteLine(nameof(step1)); 27 | 28 | await ExecuteSteps(Args); 29 | ``` 30 | 31 | We can now execute this script like 32 | 33 | ```shell 34 | csi main.csx step1 35 | ``` 36 | 37 | Or with dotnet-script like this 38 | 39 | ```shell 40 | dotnet script main.csx step1 41 | ``` 42 | 43 | > Note: The arguments passed to the script is used to determine which step(s) to execute. 44 | 45 | At the end of execution we will output a summary report. 46 | 47 | ``` 48 | --------------------------------------------------------------------- 49 | Steps Summary 50 | --------------------------------------------------------------------- 51 | Step Duration Total 52 | ----- ---------------- ---------------- 53 | step1 00:00:00.0006126 00:00:00.0006126 54 | --------------------------------------------------------------------- 55 | Total 00:00:00.0006126 56 | ``` 57 | 58 | ### Multiple Steps 59 | 60 | ```c# 61 | Step step1 = () => WriteLine(nameof(step1)); 62 | Step step2 = () => WriteLine(nameof(step2)); 63 | await ExecuteSteps(Args); 64 | ``` 65 | 66 | We can now execute both steps like this 67 | 68 | ```shell 69 | csi main.csx step1 step2 70 | ``` 71 | 72 | Which will generate a report showing the duration of each step. 73 | 74 | ``` 75 | --------------------------------------------------------------------- 76 | Steps Summary 77 | --------------------------------------------------------------------- 78 | Step Duration Total 79 | ----- ---------------- ---------------- 80 | step2 00:00:00.0000528 00:00:00.0000528 81 | step1 00:00:00.0006086 00:00:00.0006086 82 | --------------------------------------------------------------------- 83 | Total 00:00:00.0006614 84 | ``` 85 | 86 | ### Nested Steps 87 | 88 | Nesting steps is as simple as calling the step within another step. 89 | Notice that there is no `DependsOn` or any other funky DSL, just plain C# 👍 90 | 91 | ```c# 92 | Step step1 = () => WriteLine(nameof(step1)); 93 | Step step2 = () => 94 | { 95 | step1(); 96 | WriteLine(nameof(step2)); 97 | }; 98 | 99 | await ExecuteSteps(Args); 100 | ``` 101 | 102 | Looking at the summary we will see that we get a full report of executed steps even if just called `step1` from within `step2` 103 | 104 | ``` 105 | --------------------------------------------------------------------- 106 | Steps Summary 107 | --------------------------------------------------------------------- 108 | Step Duration Total 109 | ----- ---------------- ---------------- 110 | step1 00:00:00.0007010 00:00:00.0007010 111 | step2 00:00:00.0009654 00:00:00.0016664 112 | --------------------------------------------------------------------- 113 | Total 00:00:00.0016664 114 | ``` 115 | 116 | The `Duration` column shows the time spent in the step excluding the time spent calling other steps, while the `Total` column show the time spent in the step including the time spent calling other steps. 117 | 118 | The `Total` in the end of the summary is just a sum of the `Duration` column. 119 | 120 | ### Default Step 121 | 122 | When we have multiple steps in a script, we can mark a step with the `DefaultStep` attribute so that we can invoke the script without any arguments. 123 | 124 | ```c# 125 | Step step1 = () => WriteLine(nameof(step1)); 126 | 127 | [DefaultStep] 128 | Step step2 = () => 129 | { 130 | step1(); 131 | WriteLine(nameof(step2)); 132 | }; 133 | 134 | await ExecuteSteps(Args); 135 | ``` 136 | 137 | 138 | 139 | ### Async Steps 140 | 141 | If we need to call an `async` method from within a step we can do that easily by declaring an `AsyncStep` 142 | 143 | ```c# 144 | AsyncStep step1 = async () => 145 | { 146 | await Task.CompletedTask; 147 | WriteLine(nameof(step1)); 148 | }; 149 | 150 | await ExecuteSteps(Args); 151 | ``` 152 | 153 | We can of course call another steps from within an `AsyncStep`, but we should try to avoid calling an `AsyncStep` from within a `Step` as that would be a blocking operation. The general rules of `async/await` applies here as well. 154 | 155 | ### Help 156 | 157 | We can get a list of available steps by passing `help` when executing our script. 158 | 159 | ```shell 160 | csi main.csx help 161 | ``` 162 | 163 | Witch gives us a nice list of available steps. 164 | 165 | ```c# 166 | Available steps 167 | --------------------------------------------------------------------- 168 | build 169 | test 170 | publish 171 | ``` 172 | 173 | The step name might be descriptive enough as it is, but we can also provide a step description like this. 174 | 175 | ```c# 176 | [StepDescription("Builds all projects")] 177 | Step build = () => WriteLine(nameof(build)); 178 | ``` 179 | 180 | This information will be included in the help step like this. 181 | 182 | ``` 183 | --------------------------------------------------------------------- 184 | Available steps 185 | --------------------------------------------------------------------- 186 | Step Description 187 | ------ ----------- 188 | build Builds all projects 189 | test 190 | publish 191 | ``` 192 | 193 | ### Summary 194 | 195 | By default, `dotnet-step` will create a summary at the end of execution like this. 196 | 197 | ``` 198 | --------------------------------------------------------------------- 199 | Steps Summary 200 | --------------------------------------------------------------------- 201 | Step Duration Total 202 | ----- ---------------- ---------------- 203 | step2 00:00:00.0000528 00:00:00.0000528 204 | step1 00:00:00.0006086 00:00:00.0006086 205 | --------------------------------------------------------------------- 206 | Total 00:00:00.0006614 207 | ``` 208 | 209 | If we for some reason should want to remove the summary report, we can do that with a `SummaryStep` that does nothing. 210 | 211 | ```c# 212 | SummaryStep summary = (results) => {}; 213 | ``` 214 | 215 | Or if we want to format the output differently 216 | 217 | ```C# 218 | SummaryStep summary = (results) => results.ShowSummary(); 219 | ``` 220 | 221 | > Note: The `ShowSummary` method is just an `IEnumerable`extension method witch also is an excellent way to to implement a custom summary report. 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /build/build-context.csx: -------------------------------------------------------------------------------- 1 | #load "nuget: Dotnet.Build, 0.3.9" 2 | using static FileUtils; 3 | 4 | var Owner = "seesharper"; 5 | 6 | var ProjectName = "dotnet-steps"; 7 | 8 | var ScriptFolder = GetScriptFolder(); 9 | var TempFolder = CreateDirectory(ScriptFolder, "tmp"); 10 | var ContentFolder = CreateDirectory(TempFolder,"contentFiles", "csx", "any"); 11 | var PathToSourceFile = Path.Combine(ScriptFolder, "..", "src", "steps.csx"); 12 | var PathToPackageScriptFile = Path.Combine(ContentFolder, "main.csx"); 13 | 14 | var PathToNuGetMetadataSource = Path.Combine(ScriptFolder,"dotnet-steps.nuspec"); 15 | var PathToNuGetMetadataTarget = Path.Combine(TempFolder,"dotnet-steps.nuspec"); 16 | 17 | var StepsTests = Path.Combine(ScriptFolder,"..","src","steps.tests.csx"); 18 | 19 | var AsyncTests = Path.Combine(ScriptFolder,"..","src","async.steps.tests.csx"); 20 | 21 | var DefaultTests = Path.Combine(ScriptFolder,"..","src","default.tests.csx"); 22 | 23 | var DurationTests = Path.Combine(ScriptFolder,"..","src","duration.tests.csx"); 24 | 25 | var SummaryTests = Path.Combine(ScriptFolder,"..","src","custom.summary.tests.csx"); 26 | 27 | var SingleStepTests = Path.Combine(ScriptFolder,"..","src","singlestep.tests.csx"); 28 | 29 | var PathToArtifactsFolders = CreateDirectory(ScriptFolder, "Artifacts"); 30 | 31 | var NuGetArtifactsFolder = CreateDirectory(PathToArtifactsFolders, "Artifacts", "NuGet"); 32 | var GitHubArtifactsFolder = CreateDirectory(PathToArtifactsFolders, "Artifacts", "GitHub"); 33 | 34 | var PathToReleaseNotes = Path.Combine(GitHubArtifactsFolder, "ReleaseNotes.md"); 35 | 36 | -------------------------------------------------------------------------------- /build/build.csx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet-script 2 | #load "nuget: Dotnet.Build, 0.3.9" 3 | #load "nuget:github-changelog, 0.1.5" 4 | #load "build-context.csx" 5 | #load "../src/steps.csx" 6 | using static FileUtils; 7 | using static DotNet; 8 | using static ChangeLog; 9 | using static ReleaseManagement; 10 | 11 | 12 | Step test = () => 13 | { 14 | Test(StepsTests); 15 | Test(AsyncTests); 16 | Test(SummaryTests); 17 | Test(DurationTests); 18 | Test(DefaultTests); 19 | Test(SingleStepTests); 20 | }; 21 | 22 | Step pack = () => 23 | { 24 | Copy(PathToSourceFile, PathToPackageScriptFile); 25 | Copy(PathToNuGetMetadataSource, PathToNuGetMetadataTarget); 26 | NuGet.Pack(TempFolder, NuGetArtifactsFolder); 27 | }; 28 | 29 | AsyncStep changelog = async () => 30 | { 31 | Logger.Log("Creating release notes"); 32 | var generator = ChangeLogFrom(Owner, ProjectName, BuildEnvironment.GitHubAccessToken).SinceLatestTag(); 33 | if (!Git.Default.IsTagCommit()) 34 | { 35 | generator = generator.IncludeUnreleased(); 36 | } 37 | await generator.Generate(PathToReleaseNotes); 38 | }; 39 | 40 | [DefaultStep] 41 | AsyncStep deploy = async () => 42 | { 43 | test(); 44 | pack(); 45 | if (BuildEnvironment.IsSecure) 46 | { 47 | await changelog(); 48 | if (Git.Default.IsTagCommit()) 49 | { 50 | Git.Default.RequreCleanWorkingTree(); 51 | await ReleaseManagerFor(Owner, ProjectName,BuildEnvironment.GitHubAccessToken) 52 | .CreateRelease(Git.Default.GetLatestTag(), PathToReleaseNotes, Array.Empty()); 53 | NuGet.TryPush(NuGetArtifactsFolder); 54 | } 55 | } 56 | }; 57 | 58 | 59 | await StepRunner.Execute(Args); -------------------------------------------------------------------------------- /build/dotnet-steps.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dotnet-steps 5 | dotnet-steps 6 | 0.0.2 7 | A small step for man kind, but a HUGE leap for build scripts. 8 | Bernhard Richter 9 | Bernhard Richter 10 | https://github.com/seesharper/dotnet-steps 11 | https://opensource.org/licenses/MIT 12 | C# UnitTesting Scripting 13 | 14 | -------------------------------------------------------------------------------- /main.csx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet-script 2 | 3 | Console.WriteLine("Hello world!"); 4 | -------------------------------------------------------------------------------- /omnisharp.json: -------------------------------------------------------------------------------- 1 | { 2 | "script": { 3 | "enableScriptNuGetReferences": true, 4 | "defaultTargetFramework": "netcoreapp2.1" 5 | } 6 | } -------------------------------------------------------------------------------- /src/async.steps.tests.csx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet-script 2 | #load "nuget: ScriptUnit, 0.2.0" 3 | #load "steps.csx" 4 | #r "nuget: FluentAssertions, 5.5.3" 5 | using static ScriptUnit; 6 | using FluentAssertions; 7 | 8 | AsyncStep step1 = async () => WriteLine(nameof(step1)); 9 | 10 | AsyncStep step2 = async () => WriteLine(nameof(step2)); 11 | 12 | Step step3 = () => WriteLine(nameof(step3)); 13 | 14 | await new TestRunner().AddTopLevelTests().AddFilter(m => m.Name.StartsWith("ShouldShowHelpWhenThereIsNoDefaultStep")).Execute(); 15 | 16 | public async Task ShouldExecuteAsyncStep() 17 | { 18 | await ExecuteSteps(new List(){"step1"}); 19 | TestContext.StandardOut.Should().Contain("step1"); 20 | } 21 | 22 | public async Task ShouldShowHelpWhenThereIsNoDefaultStep() 23 | { 24 | await ExecuteSteps(new List()); 25 | TestContext.StandardOut.Should().Contain("Available Steps"); 26 | TestContext.StandardOut.Should().Contain("step1"); 27 | TestContext.StandardOut.Should().Contain("step2"); 28 | TestContext.StandardOut.Should().Contain("step3"); 29 | } -------------------------------------------------------------------------------- /src/custom.summary.tests.csx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet-script 2 | #r "nuget: FluentAssertions, 5.5.3" 3 | #load "nuget: ScriptUnit, 0.2.0" 4 | #load "steps.csx" 5 | 6 | 7 | using FluentAssertions; 8 | using static ScriptUnit; 9 | 10 | [DefaultStep] 11 | Step step = () => WriteLine("TEST"); 12 | 13 | SummaryStep summaryStep = results => WriteLine("Custom Summary"); 14 | 15 | await new TestRunner().AddTopLevelTests().AddFilter(m => m.Name.StartsWith("Should")).Execute(); 16 | 17 | 18 | public async Task ShouldUseCustomSummary() 19 | { 20 | await ExecuteSteps(new List()); 21 | TestContext.StandardOut.Should().Contain("Custom Summary"); 22 | } -------------------------------------------------------------------------------- /src/default.tests.csx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet-script 2 | #load "nuget: ScriptUnit, 0.2.0" 3 | #load "steps.csx" 4 | #r "nuget: FluentAssertions, 5.5.3" 5 | using static ScriptUnit; 6 | using FluentAssertions; 7 | 8 | 9 | Step step1 = () => WriteLine(nameof(step1)); 10 | 11 | Step step2 = () => WriteLine(nameof(step2)); 12 | 13 | await new TestRunner().AddTopLevelTests().AddFilter(m => m.Name.StartsWith("Should")).Execute(); 14 | 15 | public async Task ShouldShowHelpWhenThereIsNoDefaultStep() 16 | { 17 | await ExecuteSteps(new List()); 18 | TestContext.StandardOut.Should().Contain("Available Steps"); 19 | TestContext.StandardOut.Should().Contain("step1"); 20 | TestContext.StandardOut.Should().Contain("step2"); 21 | TestContext.StandardOut.Should().NotContain("Summary"); 22 | } -------------------------------------------------------------------------------- /src/duration.tests.csx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet-script 2 | #load "nuget: ScriptUnit, 0.2.0" 3 | #load "steps.csx" 4 | #r "nuget: FluentAssertions, 5.5.3" 5 | using static ScriptUnit; 6 | using FluentAssertions; 7 | using System.Threading; 8 | 9 | 10 | Step step1 = () => Thread.Sleep(100); 11 | 12 | 13 | Step step2 = () => 14 | { 15 | Thread.Sleep(100); 16 | }; 17 | 18 | Step step3 = () => 19 | { 20 | step1(); 21 | step2(); 22 | Thread.Sleep(100); 23 | }; 24 | 25 | Step step4 = () => 26 | { 27 | step3(); 28 | Thread.Sleep(100); 29 | }; 30 | 31 | AsyncStep asyncStep1 = async () => await Task.Delay(100); 32 | 33 | 34 | AsyncStep asyncStep2 = async () => 35 | { 36 | await Task.Delay(100); 37 | }; 38 | 39 | AsyncStep asyncStep3 = async () => 40 | { 41 | await asyncStep1(); 42 | await asyncStep2(); 43 | await Task.Delay(100); 44 | }; 45 | 46 | AsyncStep asyncStep4 = async () => 47 | { 48 | await asyncStep3(); 49 | await Task.Delay(100); 50 | }; 51 | 52 | private StepResult[] _results; 53 | 54 | SummaryStep summaryStep = (results) => _results = results.ToArray(); 55 | 56 | 57 | 58 | await new TestRunner().AddTopLevelTests().AddFilter(m => m.Name.StartsWith("Should")).Execute(); 59 | 60 | public async Task ShouldReportIndividualStepDurations() 61 | { 62 | await ExecuteSteps(new List(){"step3"}); 63 | TimeSpan.FromTicks(_results.Sum(r => r.Duration.Ticks)).Should().BeCloseTo(TimeSpan.FromMilliseconds(300), 50); 64 | } 65 | 66 | public async Task ShouldReportIndividualAsyncStepDurations() 67 | { 68 | await ExecuteSteps(new List(){"asyncStep3"}); 69 | TimeSpan.FromTicks(_results.Sum(r => r.Duration.Ticks)).Should().BeCloseTo(TimeSpan.FromMilliseconds(300), 50); 70 | } 71 | 72 | 73 | public async Task ShouldReportIndividualStepDurationsForNestedSteps() 74 | { 75 | await StepRunner.Execute(new List(){"step4"}); 76 | TimeSpan.FromTicks(_results.Sum(r => r.Duration.Ticks)).Should().BeCloseTo(TimeSpan.FromMilliseconds(400), 50); 77 | } 78 | 79 | public async Task ShouldReportIndividualAsyncStepDurationsForNestedSteps() 80 | { 81 | await StepRunner.Execute(new List(){"asyncStep4"}); 82 | TimeSpan.FromTicks(_results.Sum(r => r.Duration.Ticks)).Should().BeCloseTo(TimeSpan.FromMilliseconds(400), 50); 83 | } 84 | -------------------------------------------------------------------------------- /src/singlestep.tests.csx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet-script 2 | #load "nuget: ScriptUnit, 0.2.0" 3 | #load "steps.csx" 4 | #r "nuget: FluentAssertions, 5.5.3" 5 | using static ScriptUnit; 6 | using FluentAssertions; 7 | using System.Threading; 8 | 9 | Step step1 = () => WriteLine(nameof(step1)); 10 | 11 | await new TestRunner().AddTopLevelTests().AddFilter(m => m.Name.StartsWith("Should")).Execute(); 12 | 13 | public async Task ShouldUseSingleStepAsDefault() 14 | { 15 | await ExecuteSteps(new List()); 16 | TestContext.StandardOut.Should().Contain("Summary"); 17 | TestContext.StandardOut.Should().Contain("step1"); 18 | } -------------------------------------------------------------------------------- /src/steps.csx: -------------------------------------------------------------------------------- 1 | /********************************************************************************* 2 | The MIT License (MIT) 3 | Copyright (c) 2018 bernhard.richter@gmail.com 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 18 | SOFTWARE. 19 | ****************************************************************************** 20 | dotnet-steps version 0.0.1 21 | https://github.com/seesharper/dotnet-steps 22 | http://twitter.com/bernhardrichter 23 | ******************************************************************************/ 24 | using System.ComponentModel; 25 | using System.Reflection; 26 | 27 | /// 28 | /// Represents a synchronous step. 29 | /// 30 | public delegate void Step(); 31 | 32 | /// 33 | /// Represents an asynchronous step. 34 | /// 35 | /// 36 | public delegate Task AsyncStep(); 37 | 38 | /// 39 | /// Represents a function that displays a summary report. 40 | /// 41 | /// 42 | public delegate void SummaryStep(IEnumerable results); 43 | 44 | // Dummy lambda to obtain the submission instance. 45 | Action stepsDummyaction = () => StepsDummy(); 46 | 47 | void StepsDummy() { } 48 | 49 | 50 | StepRunner.Initialize(stepsDummyaction.Target); 51 | 52 | public static async Task ExecuteSteps(IList args) 53 | { 54 | await StepRunner.Execute(args); 55 | } 56 | 57 | 58 | public static void ShowHelp(this StepInfo[] steps) 59 | { 60 | WriteLine("---------------------------------------------------------------------"); 61 | WriteLine("Available Steps"); 62 | WriteLine("---------------------------------------------------------------------"); 63 | var stepMaxWidth = steps.Select(s => $"{s.Name}".Length).OrderBy(l => l).Last() + 15; 64 | WriteLine($"{"Step".PadRight(stepMaxWidth)}Description"); 65 | WriteLine($"{"".PadRight(stepMaxWidth - 15, '-')}{"".PadLeft(15)}{"".PadRight(18, '-')}"); 66 | foreach (var step in steps) 67 | { 68 | var name = step.Name + (step.IsDefault ? " (default)" : string.Empty); 69 | Write(name.PadRight(stepMaxWidth, ' ')); 70 | WriteLine(step.Description); 71 | } 72 | } 73 | 74 | public static void ShowSummary(this StepResult[] results) 75 | { 76 | if (results.Length == 0) 77 | { 78 | return; 79 | } 80 | 81 | WriteLine("---------------------------------------------------------------------"); 82 | WriteLine("Steps Summary"); 83 | WriteLine("---------------------------------------------------------------------"); 84 | var stepMaxWidth = results.Select(s => $"{s.Name}".Length).OrderBy(l => l).Last() + 15; 85 | WriteLine($"{"Step".PadRight(stepMaxWidth)}Duration{"".PadLeft(10)} Total"); 86 | 87 | WriteLine($"{"".PadRight(stepMaxWidth - 15, '-')}{"".PadLeft(15)}{"".PadRight(16, '-')}{"".PadLeft(3)}{"".PadRight(16, '-')}"); 88 | TimeSpan total = TimeSpan.Zero; 89 | foreach (var result in results) 90 | { 91 | total = total.Add(result.Duration); 92 | WriteLine($"{result.Name.PadRight(stepMaxWidth)}{result.Duration.ToString()}{"".PadLeft(3)}{result.TotalDuration.ToString()}"); 93 | } 94 | WriteLine("---------------------------------------------------------------------"); 95 | WriteLine($"{"Total".PadRight(stepMaxWidth)}{total.ToString()}"); 96 | } 97 | 98 | [DebuggerStepThrough] 99 | private static class StepRunner 100 | { 101 | private static object _submission; 102 | private static Type _submissionType; 103 | 104 | private static Stack _callStack = new Stack(); 105 | 106 | private static List _results = new List(); 107 | 108 | private static bool HasWrappedFields; 109 | 110 | 111 | [EditorBrowsable(EditorBrowsableState.Never)] 112 | internal static void Initialize(object submission) 113 | { 114 | _submission = submission; 115 | _submissionType = submission.GetType(); 116 | } 117 | 118 | public async static Task Execute(IList stepNames) 119 | { 120 | if (!HasWrappedFields) 121 | { 122 | WrapFields(_results); 123 | HasWrappedFields = true; 124 | } 125 | 126 | var stepDelegates = GetStepDelegates(); 127 | 128 | if (stepNames.Contains("help", StringComparer.OrdinalIgnoreCase)) 129 | { 130 | ShowHelp(stepDelegates.Values.ToArray()); 131 | return; 132 | } 133 | 134 | if (stepDelegates.Keys.Intersect(stepNames).Count() == 0) 135 | { 136 | await GetDefaultDelegate(stepDelegates)(); 137 | } 138 | 139 | foreach (var stepName in stepNames) 140 | { 141 | _callStack.Clear(); 142 | 143 | // if (stepName.Equals("help", StringComparison.OrdinalIgnoreCase)) 144 | // { 145 | // stepDelegates.Values.ToArray().ShowHelp(); 146 | // break; 147 | // } 148 | 149 | if (stepDelegates.TryGetValue(stepName, out var stepDelegate)) 150 | { 151 | await stepDelegate.Invoke(); 152 | continue; 153 | } 154 | } 155 | 156 | GetSummaryStepDelegate()(_results); 157 | _results.Clear(); 158 | } 159 | 160 | private static void WrapFields(List results) 161 | { 162 | WrapStepFields(); 163 | WrapAsyncStepFields(results); 164 | } 165 | 166 | private static void WrapStepFields() 167 | { 168 | var stepFields = GetStepFields(); 169 | foreach (var stepField in stepFields) 170 | { 171 | var step = GetStepDelegate(stepField); 172 | Step wrappedStep = () => 173 | { 174 | StepResult stepresult = PushStepResultOntoCallStack(stepField); 175 | var stopWatch = Stopwatch.StartNew(); 176 | step(); 177 | stopWatch.Stop(); 178 | PopCallStackAndUpdateDurations(stepresult, stopWatch); 179 | }; 180 | stepField.SetValue(stepField.IsStatic ? null : _submission, wrappedStep); 181 | } 182 | } 183 | 184 | private static void WrapAsyncStepFields(List results) 185 | { 186 | var stepFields = GetStepFields(); 187 | foreach (var stepField in stepFields) 188 | { 189 | var step = GetStepDelegate(stepField); 190 | AsyncStep wrappedStep = async () => 191 | { 192 | StepResult stepresult = PushStepResultOntoCallStack(stepField); 193 | var stopWatch = Stopwatch.StartNew(); 194 | await step(); 195 | stopWatch.Stop(); 196 | PopCallStackAndUpdateDurations(stepresult, stopWatch); 197 | }; 198 | stepField.SetValue(stepField.IsStatic ? null : _submission, wrappedStep); 199 | } 200 | } 201 | 202 | private static StepResult PushStepResultOntoCallStack(FieldInfo stepField) 203 | { 204 | var stepresult = new StepResult(stepField.Name, TimeSpan.Zero, TimeSpan.Zero); 205 | _callStack.Push(stepresult); 206 | return stepresult; 207 | } 208 | 209 | private static void PopCallStackAndUpdateDurations(StepResult stepresult, Stopwatch stopWatch) 210 | { 211 | var durationForThisStep = stopWatch.Elapsed; 212 | stepresult.TotalDuration = durationForThisStep; 213 | _results.Add(_callStack.Pop()); 214 | 215 | if (_callStack.Count > 0) 216 | { 217 | var callingStep = _callStack.Peek(); 218 | callingStep.Duration = callingStep.Duration.Subtract(durationForThisStep); 219 | } 220 | stepresult.Duration = stepresult.Duration.Add(durationForThisStep); 221 | } 222 | 223 | private static SummaryStep GetSummaryStepDelegate() 224 | { 225 | var summarySteps = GetStepDelegates(); 226 | if (summarySteps.Length > 1) 227 | { 228 | throw new InvalidOperationException("Found multiple summary steps"); 229 | } 230 | 231 | if (summarySteps.Length == 1) 232 | { 233 | return summarySteps[0]; 234 | } 235 | 236 | return results => results.ToArray().ShowSummary(); 237 | } 238 | 239 | private static Func GetDefaultDelegate(Dictionary stepDelegates) 240 | { 241 | if (stepDelegates.Count == 1) 242 | { 243 | return stepDelegates.First().Value.Invoke; 244 | } 245 | 246 | var defaultStepDelegate = stepDelegates.Values.Where(si => si.IsDefault).SingleOrDefault(); 247 | if (defaultStepDelegate != null) 248 | { 249 | return defaultStepDelegate.Invoke; 250 | } 251 | 252 | return () => 253 | { 254 | stepDelegates.Values.ToArray().ShowHelp(); 255 | return Task.CompletedTask; 256 | }; 257 | 258 | } 259 | 260 | private static Dictionary GetStepDelegates() 261 | { 262 | var stepFields = GetStepFields(); 263 | List results = new List(); 264 | foreach (var stepField in stepFields) 265 | { 266 | StepInfo stepInfo = new StepInfo(stepField.Name, GetStepDescription(stepField), RepresentsDefaultStep(stepField), () => { GetStepDelegate(stepField)(); return Task.CompletedTask; }); 267 | results.Add(stepInfo); 268 | } 269 | 270 | var asyncStepFields = GetStepFields(); 271 | foreach (var asyncStepField in asyncStepFields) 272 | { 273 | StepInfo stepInfo = new StepInfo(asyncStepField.Name, GetStepDescription(asyncStepField), RepresentsDefaultStep(asyncStepField), () => GetStepDelegate(asyncStepField)()); 274 | results.Add(stepInfo); 275 | } 276 | 277 | return results.ToDictionary(si => si.Name, si => si, StringComparer.OrdinalIgnoreCase); 278 | } 279 | 280 | private static TStep GetStepDelegate(FieldInfo stepField) 281 | { 282 | return (TStep)(stepField.IsStatic ? stepField.GetValue(null) : stepField.GetValue(_submission)); 283 | } 284 | 285 | private static TStep GetStepDelegate(PropertyInfo property) 286 | { 287 | return (TStep)(property.GetMethod.IsStatic ? property.GetValue(null) : property.GetValue(_submission)); 288 | } 289 | 290 | private static TStep[] GetStepDelegates() 291 | { 292 | var fieldSteps = GetStepFields().Select(f => GetStepDelegate(f)); 293 | var propertySteps = GetStepProperties().Select(p => GetStepDelegate(p)); 294 | return fieldSteps.Concat(propertySteps).ToArray(); 295 | } 296 | 297 | private static string GetStepDescription(MemberInfo stepFieldInfo) 298 | { 299 | return stepFieldInfo.GetCustomAttribute()?.Description ?? string.Empty; 300 | } 301 | 302 | private static bool RepresentsDefaultStep(MemberInfo memberInfo) 303 | { 304 | return memberInfo.IsDefined(typeof(DefaultStepAttribute)) || memberInfo.Name.Equals("defaultstep", StringComparison.OrdinalIgnoreCase); 305 | } 306 | 307 | private static FieldInfo[] GetStepFields() 308 | { 309 | return _submissionType.GetFields().Where(f => f.FieldType == typeof(TStep)).ToArray(); 310 | } 311 | 312 | private static PropertyInfo[] GetStepProperties() 313 | { 314 | return _submissionType.GetProperties().Where(f => f.PropertyType == typeof(TStep)).ToArray(); 315 | } 316 | } 317 | 318 | [AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property, Inherited = false, AllowMultiple = false)] 319 | public sealed class StepDescriptionAttribute : Attribute 320 | { 321 | public StepDescriptionAttribute(string description) 322 | { 323 | Description = description; 324 | } 325 | 326 | public string Description { get; } 327 | } 328 | 329 | [AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Property, Inherited = false, AllowMultiple = false)] 330 | public sealed class DefaultStepAttribute : Attribute 331 | { 332 | public DefaultStepAttribute() 333 | { 334 | } 335 | } 336 | 337 | public class StepResult 338 | { 339 | public StepResult(string name, TimeSpan duration, TimeSpan totalDuration) 340 | { 341 | Name = name; 342 | Duration = duration; 343 | TotalDuration = totalDuration; 344 | } 345 | 346 | public string Name { get; } 347 | public TimeSpan Duration { get; set; } 348 | public TimeSpan TotalDuration { get; set; } 349 | } 350 | 351 | 352 | public class StepInfo 353 | { 354 | private readonly Func _step; 355 | 356 | public StepInfo(string name, string description, bool isDefault, Func step) 357 | { 358 | Name = name; 359 | Description = description; 360 | IsDefault = isDefault; 361 | _step = step; 362 | } 363 | 364 | public string Name { get; } 365 | public string Description { get; } 366 | public bool IsDefault { get; } 367 | 368 | public async Task Invoke() 369 | { 370 | await _step(); 371 | } 372 | } -------------------------------------------------------------------------------- /src/steps.tests.csx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet-script 2 | #load "nuget: ScriptUnit, 0.2.0" 3 | #load "steps.csx" 4 | #r "nuget: FluentAssertions, 5.5.3" 5 | using static ScriptUnit; 6 | using FluentAssertions; 7 | using System.Threading; 8 | 9 | [StepDescription("This is step one")] 10 | [DefaultStep] 11 | Step step1 = () => WriteLine("nameof(step1)"); 12 | 13 | Step step2 = () => { 14 | step1(); 15 | WriteLine("nameof(step2)"); 16 | }; 17 | 18 | Step step3 = () => { 19 | step1(); 20 | step2(); 21 | WriteLine("nameof(step3)"); 22 | }; 23 | 24 | 25 | await new TestRunner().AddTopLevelTests().AddFilter(m => m.Name.StartsWith("Should")).Execute(); 26 | 27 | static List EmptyArgs = new List(); 28 | 29 | public async Task ShouldExecuteStep() 30 | { 31 | await ExecuteSteps(new List(){"step1"}); 32 | TestContext.StandardOut.Should().Contain("step1"); 33 | } 34 | 35 | public async Task ShouldExecuteDefaultStep() 36 | { 37 | await ExecuteSteps(new List()); 38 | TestContext.StandardOut.Should().Contain("step1"); 39 | } 40 | 41 | public async Task ShouldShowHelp() 42 | { 43 | await ExecuteSteps(new List(){"help"}); 44 | TestContext.StandardOut.Should().Contain("Available Steps"); 45 | TestContext.StandardOut.Should().Contain("This is step one"); 46 | TestContext.StandardOut.Should().Contain("step1 (default)"); 47 | TestContext.StandardOut.Should().NotContain("Steps Summary"); 48 | } 49 | 50 | public async Task ShouldReportNestedStep() 51 | { 52 | await ExecuteSteps(new List(){"step2"}); 53 | TestContext.StandardOut.Should().Contain("step1"); 54 | TestContext.StandardOut.Should().Contain("step2"); 55 | } 56 | 57 | public async Task ShouldMarkDefaultStepInHelp() 58 | { 59 | await ExecuteSteps(new List(){"help"}); 60 | TestContext.StandardOut.Should().Contain("step1 (default)"); 61 | TestContext.StandardOut.Should().Contain("step2"); 62 | TestContext.StandardOut.Should().Contain("step3"); 63 | } 64 | 65 | public async Task ShouldHandleCallingSameStepTwice() 66 | { 67 | await StepRunner.Execute(new List(){"step3"}); 68 | } 69 | 70 | 71 | --------------------------------------------------------------------------------