From d71c29d846330fcd5fde48198ce37fc81b61248e Mon Sep 17 00:00:00 2001 From: woksin Date: Wed, 29 Jul 2026 01:49:27 +0200 Subject: [PATCH 1/4] Add CompileFile to the play file compiler Compiling one known file went through directory discovery, so a caller with a path in hand had nowhere to go. CompileFile compiles it directly and uses the file name as the relative path, so diagnostics read the same way they do for a discovered file. --- .../Screenplay/Files/IPlayFileCompiler.cs | 13 +++++++++++- .../Screenplay/Files/PlayFileCompiler.cs | 20 +++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/Source/DotNET/Screenplay/Files/IPlayFileCompiler.cs b/Source/DotNET/Screenplay/Files/IPlayFileCompiler.cs index ebed6f8..93c3713 100644 --- a/Source/DotNET/Screenplay/Files/IPlayFileCompiler.cs +++ b/Source/DotNET/Screenplay/Files/IPlayFileCompiler.cs @@ -4,7 +4,7 @@ namespace Cratis.Screenplay.Files; /// -/// Defines a system that discovers and compiles every .play file beneath a directory. +/// Defines a system that compiles .play files - a single file, or every file beneath a directory. /// public interface IPlayFileCompiler { @@ -14,4 +14,15 @@ public interface IPlayFileCompiler /// The root directory to search from. /// A per discovered file. IEnumerable CompileIn(string root); + + /// + /// Compiles a single .play file. + /// + /// The path of the file to compile. + /// The of the file. + /// + /// The resulting is the file name, so diagnostics read the same + /// way they do for a discovered file. + /// + PlayFileCompilation CompileFile(string path); } diff --git a/Source/DotNET/Screenplay/Files/PlayFileCompiler.cs b/Source/DotNET/Screenplay/Files/PlayFileCompiler.cs index 0e42cc7..39ad58b 100644 --- a/Source/DotNET/Screenplay/Files/PlayFileCompiler.cs +++ b/Source/DotNET/Screenplay/Files/PlayFileCompiler.cs @@ -20,10 +20,18 @@ public PlayFileCompiler() /// public IEnumerable CompileIn(string root) => - [.. playFiles.FindIn(root) - .Select(file => - { - var source = playFiles.ReadContent(file); - return new PlayFileCompilation(file, source, compiler.Compile(source)); - })]; + [.. playFiles.FindIn(root).Select(Compile)]; + + /// + public PlayFileCompilation CompileFile(string path) + { + var full = System.IO.Path.GetFullPath(path); + return Compile(new(full, System.IO.Path.GetFileName(full))); + } + + PlayFileCompilation Compile(PlayFile file) + { + var source = playFiles.ReadContent(file); + return new(file, source, compiler.Compile(source)); + } } From 26c69553db90b7ace47adab993484a2982da9eec Mon Sep 17 00:00:00 2001 From: woksin Date: Wed, 29 Jul 2026 01:49:27 +0200 Subject: [PATCH 2/4] Add specs for compiling a single play file --- .../given/a_play_file_compiler.cs | 18 +++++++++++ .../when_compiling_a_single_file.cs | 31 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 Source/DotNET/Screenplay/for_PlayFileCompiler/given/a_play_file_compiler.cs create mode 100644 Source/DotNET/Screenplay/for_PlayFileCompiler/when_compiling_a_single_file.cs diff --git a/Source/DotNET/Screenplay/for_PlayFileCompiler/given/a_play_file_compiler.cs b/Source/DotNET/Screenplay/for_PlayFileCompiler/given/a_play_file_compiler.cs new file mode 100644 index 0000000..9553e55 --- /dev/null +++ b/Source/DotNET/Screenplay/for_PlayFileCompiler/given/a_play_file_compiler.cs @@ -0,0 +1,18 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.Screenplay.Files; + +namespace Cratis.Screenplay.for_PlayFileCompiler.given; + +public class a_play_file_compiler : Specification +{ + protected IPlayFiles _playFiles; + protected PlayFileCompiler _compiler; + + void Establish() + { + _playFiles = Substitute.For(); + _compiler = new(_playFiles, new ScreenplayCompiler()); + } +} diff --git a/Source/DotNET/Screenplay/for_PlayFileCompiler/when_compiling_a_single_file.cs b/Source/DotNET/Screenplay/for_PlayFileCompiler/when_compiling_a_single_file.cs new file mode 100644 index 0000000..e037b92 --- /dev/null +++ b/Source/DotNET/Screenplay/for_PlayFileCompiler/when_compiling_a_single_file.cs @@ -0,0 +1,31 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.Screenplay.Files; + +namespace Cratis.Screenplay.for_PlayFileCompiler; + +public class when_compiling_a_single_file : given.a_play_file_compiler +{ + const string Source = + """ + module Invoicing + feature InvoiceManagement + slice StateChange RegisterInvoice + command RegisterInvoice + invoiceId Uuid + """; + + PlayFileCompilation _compilation; + + void Establish() => _playFiles.ReadContent(Arg.Any()).Returns(Source); + + void Because() => _compilation = _compiler.CompileFile(Path.Combine("some", "where", "invoicing.play")); + + [Fact] void should_not_discover_any_files() => _playFiles.DidNotReceive().FindIn(Arg.Any()); + [Fact] void should_compile_the_file() => _compilation.Result.Success.ShouldBeTrue(); + [Fact] void should_have_no_diagnostics() => _compilation.Result.Diagnostics.ShouldBeEmpty(); + [Fact] void should_keep_the_source() => _compilation.Source.ShouldEqual(Source); + [Fact] void should_use_the_file_name_as_the_relative_path() => _compilation.File.RelativePath.ShouldEqual("invoicing.play"); + [Fact] void should_resolve_the_full_path() => Path.IsPathFullyQualified(_compilation.File.Path).ShouldBeTrue(); +} From 12d591ed11ca39959f9e3186d7263b3fa98bd2a3 Mon Sep 17 00:00:00 2001 From: woksin Date: Wed, 29 Jul 2026 01:49:27 +0200 Subject: [PATCH 3/4] Accept a single .play file and add --warnaserror to the tool Pointing the tool at a file printed "Directory '' does not exist", so verifying one generated document meant copying it into a scratch directory first. A positional argument that is an existing .play file now compiles just that file; anything else keeps the directory behavior. Warnings never influenced the exit code, which hides a warning from a pipeline that generates documents and expects them clean. --warnaserror opts into failing the run on warnings, matching the convention of other compiler tools. --- Source/DotNET/Tool/Program.cs | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/Source/DotNET/Tool/Program.cs b/Source/DotNET/Tool/Program.cs index 98e2b58..ee1e3ec 100644 --- a/Source/DotNET/Tool/Program.cs +++ b/Source/DotNET/Tool/Program.cs @@ -4,10 +4,19 @@ using Cratis.Screenplay.Diagnostics; using Cratis.Screenplay.Files; -var root = args.FirstOrDefault(arg => !arg.StartsWith('-')) ?? Directory.GetCurrentDirectory(); -if (!Directory.Exists(root)) +const string PlayExtension = ".play"; + +var target = args.FirstOrDefault(arg => !arg.StartsWith('-')) ?? Directory.GetCurrentDirectory(); +var isFile = File.Exists(target); +if (!isFile && !Directory.Exists(target)) { - Console.Error.WriteLine($"Directory '{root}' does not exist"); + Console.Error.WriteLine($"'{target}' does not exist"); + return 1; +} + +if (isFile && !target.EndsWith(PlayExtension, StringComparison.OrdinalIgnoreCase)) +{ + Console.Error.WriteLine($"'{target}' is not a {PlayExtension} file"); return 1; } @@ -15,10 +24,16 @@ Environment.GetEnvironmentVariable("NO_COLOR") is null && !args.Contains("--no-color"); -var compilations = new PlayFileCompiler().CompileIn(root).ToArray(); +var warnAsError = args.Contains("--warnaserror"); + +var playFileCompiler = new PlayFileCompiler(); +var compilations = isFile + ? [playFileCompiler.CompileFile(target)] + : playFileCompiler.CompileIn(target).ToArray(); + if (compilations.Length == 0) { - Console.WriteLine($"No .play files found beneath {root}"); + Console.WriteLine($"No {PlayExtension} files found beneath {target}"); return 0; } @@ -49,11 +64,12 @@ Console.WriteLine(); } +var failed = errors > 0 || (warnAsError && warnings > 0); var summary = $"{compilations.Length} file(s) compiled - {errors} error(s), {warnings} warning(s)"; if (useColors) { var color = "\e[32m"; - if (errors > 0) + if (failed) { color = "\e[31m"; } @@ -69,4 +85,4 @@ Console.WriteLine(summary); } -return errors > 0 ? 1 : 0; +return failed ? 1 : 0; From 1df1be370d265a76c127b8b9c54f2ec08bc567ed Mon Sep 17 00:00:00 2001 From: woksin Date: Wed, 29 Jul 2026 01:49:27 +0200 Subject: [PATCH 4/4] Document single-file compilation and --warnaserror --- Documentation/screenplay/tool.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/Documentation/screenplay/tool.md b/Documentation/screenplay/tool.md index e48ea79..2d436e3 100644 --- a/Documentation/screenplay/tool.md +++ b/Documentation/screenplay/tool.md @@ -26,6 +26,12 @@ You can also point it at a specific directory: screenplay path/to/screenplays ``` +Or at a single file, which is what you want when a generator just produced one and you only care about that one: + +```bash +screenplay path/to/invoicing.play +``` + Each problem is reported with its file, line and column, the offending source line and a caret pointing at the exact location: ```text @@ -36,7 +42,20 @@ nested/broken.play(3,5): error: Unknown slice type 'Wat' - expected StateChange, 2 file(s) compiled - 1 error(s), 0 warning(s) ``` -The exit code is `0` when everything compiles without errors and `1` otherwise, so the command slots straight into CI pipelines. Colors are enabled automatically on interactive terminals; disable them with `--no-color` or by setting the `NO_COLOR` environment variable. +The exit code is `0` when everything compiles without errors and `1` otherwise, so the command slots straight into CI pipelines. + +Warnings do not fail the run by default. When a pipeline demands a spotless document - a generated one, say - add `--warnaserror` and a single warning is enough to exit `1`: + +```bash +screenplay path/to/invoicing.play --warnaserror +``` + +| Option | Effect | +|---|---| +| `--warnaserror` | Warnings fail the run - exit code `1` even with zero errors | +| `--no-color` | Never colorize output | + +Colors are enabled automatically on interactive terminals; disable them with `--no-color` or by setting the `NO_COLOR` environment variable. ## Use the compiler as a library @@ -70,4 +89,10 @@ using Cratis.Screenplay.Files; var compilations = new PlayFileCompiler().CompileIn(rootDirectory); ``` +A single file is a single call too: + +```csharp +var compilation = new PlayFileCompiler().CompileFile(path); +``` + To go the other way - turn a syntax tree back into `.play` text, or generate Screenplay from a model - see [Printing and generating](printing.md).