sevgi-showcase 0.95.0 → 0.98.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +125 -0
  3. data/README.md +9 -17
  4. data/lib/sevgi/showcase/dark.rb +4 -0
  5. data/lib/sevgi/showcase/minitest/script.rb +59 -57
  6. data/lib/sevgi/showcase/minitest/shell.rb +132 -128
  7. data/lib/sevgi/showcase/minitest/suite.rb +48 -46
  8. data/lib/sevgi/showcase/minitest.rb +6 -0
  9. data/lib/sevgi/showcase/version.rb +1 -1
  10. data/lib/sevgi/showcase.rb +0 -3
  11. data/srv/checkers.sevgi +79 -0
  12. data/srv/checkers.svg +117 -0
  13. data/srv/clover.sevgi +19 -0
  14. data/srv/clover.svg +11 -0
  15. data/srv/clover.yml +3 -0
  16. data/srv/copperplate.sevgi +50 -0
  17. data/srv/copperplate.svg +99 -0
  18. data/srv/copperplate.yml +5 -0
  19. data/srv/{gear-wheel.sevgi → gear.sevgi} +7 -9
  20. data/srv/gear.svg +11 -0
  21. data/srv/gear.yml +2 -0
  22. data/srv/{grid-cells.sevgi → grid.sevgi} +9 -9
  23. data/srv/{grid-cells.svg → grid.svg} +13 -1
  24. data/srv/heart.sevgi +17 -0
  25. data/srv/{heart-mask.svg → heart.svg} +3 -3
  26. data/srv/heart.yml +3 -0
  27. data/srv/logo.sevgi +39 -0
  28. data/srv/logo.svg +29 -0
  29. data/srv/logo.yml +4 -0
  30. data/srv/logos.sevgi +34 -0
  31. data/srv/logos.svg +38 -0
  32. data/srv/logos.yml +4 -0
  33. data/srv/{meter-face.sevgi → meter.sevgi} +6 -6
  34. data/srv/meter.svg +18 -0
  35. data/srv/pacman.sevgi +18 -0
  36. data/srv/pacman.svg +15 -0
  37. data/srv/pokey.sevgi +14 -0
  38. data/srv/pokey.svg +6 -0
  39. data/srv/{ruler-line.sevgi → ruler-.sevgi} +7 -7
  40. data/srv/ruler.sevgi +61 -0
  41. data/srv/ruler.svg +445 -0
  42. data/srv/{snow-flake.sevgi → snowflake.sevgi} +2 -2
  43. data/srv/{snow-flake.svg → snowflake.svg} +1 -1
  44. data/srv/snowflake.yml +2 -0
  45. data/srv/squared.sevgi +42 -0
  46. data/srv/squared.svg +71 -0
  47. data/srv/squared.yml +4 -0
  48. data/srv/stars.sevgi +19 -0
  49. data/srv/stars.svg +30 -0
  50. data/srv/stars.yml +3 -0
  51. data/srv/tulips.sevgi +27 -0
  52. data/srv/tulips.svg +19 -0
  53. data/srv/tulips.yml +3 -0
  54. metadata +57 -36
  55. data/srv/checker-board.sevgi +0 -96
  56. data/srv/checker-board.svg +0 -117
  57. data/srv/gear-wheel.svg +0 -12
  58. data/srv/gear-wheel.yml +0 -3
  59. data/srv/heart-mask.sevgi +0 -17
  60. data/srv/heart-mask.yml +0 -3
  61. data/srv/meter-face.svg +0 -13
  62. data/srv/pacman-pokey.sevgi +0 -17
  63. data/srv/pacman-pokey.svg +0 -9
  64. data/srv/pacman-single.sevgi +0 -17
  65. data/srv/pacman-single.svg +0 -14
  66. data/srv/ruler-hline.sevgi +0 -55
  67. data/srv/ruler-hline.svg +0 -443
  68. data/srv/snow-flake.yml +0 -1
  69. /data/srv/{checker-board.yml → checkers.yml} +0 -0
  70. /data/srv/{grid-cells.yml → grid.yml} +0 -0
  71. /data/srv/{meter-face.yml → meter.yml} +0 -0
  72. /data/srv/{pacman-pokey.yml → pacman.yml} +0 -0
  73. /data/srv/{pacman-single.yml → pokey.yml} +0 -0
  74. /data/srv/{ruler-line.svg → ruler-.svg} +0 -0
  75. /data/srv/{ruler-hline.yml → ruler-.yml} +0 -0
  76. /data/srv/{ruler-line.yml → ruler.yml} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3b056b346721f8c0dc42b08402803516bb6890e5999a399acf083842c5ad1f4
4
- data.tar.gz: 475358290666f23737aaef97a31efbb31fa96548843eeecf03cd9c2fba55c2a6
3
+ metadata.gz: 5cfa0ec43c375bc4dadcf8a6c9ffbfcae8e4fbab1d967207185e7160462eab36
4
+ data.tar.gz: 5a57c57405839b8cce479e7dc6b9f5eff9ed62a9c3a42ef51ff836761b8528e3
5
5
  SHA512:
6
- metadata.gz: 0fb1f3bc3a07900c83d3c3e32b916ac9ad87d38eb4c9b28e564ceafabae362353adf3b62c3ad93185e4b7846a0c1ef797b949dc3487c6d21025742255c755c69
7
- data.tar.gz: f0a0169c177354f9ffcc3982f54c7873e0ab1d78577989dee603bcb40ca6ecc0fa2eb36ea60c91a8fab906a21a8b7f0991dbe012942237e80236e283dd640b5d
6
+ metadata.gz: 2edf1a1607b43a582c3a8b35be8a77c81d0768715b25542a630a86cb6f10b94df2510a3ec64ab143690c75e82de2efab5b80c6911ec34300cf4329501b76dd34
7
+ data.tar.gz: 5d5dc597fc58f6e7a3995b4f13a289ff3766f9452c64d50886086ddb9d8d6731f53de4cd8fd48643fb8d3844e93f746c3a9c1727540c22190a66fb156f8803a8
data/CHANGELOG.md CHANGED
@@ -5,6 +5,131 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.98.2 - 2026-07-25
9
+
10
+ ### Fixed
11
+
12
+ - Restored the `sevgi` CLI's main-object scope so bare toolkit calls remain available inside script helper classes.
13
+
14
+ ## 0.98.1 - 2026-07-20
15
+
16
+ ### Changed
17
+
18
+ - Changed the `sevgi` CLI to use the executor's isolated scope by default.
19
+
20
+ ### Removed
21
+
22
+ - Removed the redundant `-n` and `--nomain` CLI options; library consumers can still select `main: true` through
23
+ `Sevgi.execute` and `Sevgi.execute_file` when needed.
24
+
25
+ ## 0.98.0 - 2026-07-20
26
+
27
+ ### Added
28
+
29
+ - Added standard-input support to `sevgi`, `igves`, and `igsev`; `sevgi --as NAME` and
30
+ `Sevgi.execute_file(..., as: NAME)` supply the logical source basename used by implicit SVG, PDF, and PNG
31
+ destinations without changing physical load identity.
32
+
33
+ ## 0.97.0 - 2026-07-19
34
+
35
+ ### Added
36
+
37
+ - Added `sevgi-appendix`, containing the Sevgi agent skill and a RuboCop plugin for preserving readable `.sevgi` DSL
38
+ source alongside rubyfmt-formatted Ruby; the umbrella `sevgi` gem installs the matching Appendix version.
39
+ - Added `sevgi --skill` to report the validated path of the matching packaged agent skill for product-neutral setup.
40
+ - Added `igsev` to the umbrella gem for normalizing an SVG file through a complete SVG-to-Sevgi-to-SVG round trip.
41
+
42
+ ### Changed
43
+
44
+ - Made Homebrew the recommended complete CLI installation while retaining Bundler and focused component gems for Ruby
45
+ application dependencies.
46
+
47
+ ## 0.96.0 - 2026-07-18
48
+
49
+ ### Added
50
+
51
+ - Added exact, subtree-wide attribute omission to Derender content/file conversion, evaluation, and inclusion APIs;
52
+ `igves --omit` exposes the same behavior from the command line.
53
+ - Added `Sevgi.SVG` as the explicit namespaced form of the top-level SVG document entrypoint.
54
+ - Added the opt-in recursive `SVG::Modules` contract for callable drawing namespaces.
55
+ - Added callable drawing-module `base` blocks, document and paper registry introspection, Canvas structural equality,
56
+ renderer options on `RenderChildren`, axis translation helpers, and a CC BY RDF helper.
57
+ - Formalized non-rendering `-` metadata and `+` attribute updates as `Attributes::META_PREFIX` and
58
+ `Attributes::UPDATE_SUFFIX`; repeated Array updates now concatenate into a stable flat value.
59
+
60
+ ### Changed
61
+
62
+ - Breaking: replaced the `SVG = Sevgi::Graphics` alias with an independent SVG facade. Facade operations now use
63
+ capitalized names such as `SVG.Canvas` and `SVG.Document`; lowercase component helpers remain on
64
+ `Sevgi::Graphics`, and the stuttering `SVG.SVG` form was removed.
65
+ - Breaking: made low-level `Executor.execute` and `execute_file` runners private; consumers use
66
+ `Sevgi.execute` and `Sevgi.execute_file` while retaining the public result and error types.
67
+ - Breaking: removed the abstract `Document::Base` layer from the selectable document-profile registry; `Minimal` and
68
+ `Default` are now sibling concrete profiles, and custom profile hierarchies should derive from `Base`.
69
+ - Breaking: renamed Parallelogram segment and constraint arguments by geometric role: `base`, `side`, and
70
+ `constraint` replace axis-implying constructor names.
71
+ - Made degree-based sine and cosine exact at integer quarter turns, eliminating cardinal Geometry residue.
72
+ - Documented complete file-system failure families for file comparison, output, touch, and upward location helpers.
73
+ - Formalized generated Tile ids, positional CSS classes, template placement, and per-use callback signatures.
74
+ - Formalized String and Symbol ids across Derender selection APIs and documented file-read failures consistently.
75
+ - Breaking: made top-level `Decompile`, `Derender`, `Evaluate`, and `EvaluateChildren` consume inline SVG/XML;
76
+ file inputs now use the corresponding `File`-suffixed entrypoints.
77
+ - Breaking: replaced callable drawing module `call` block registration with argument-independent `base` blocks;
78
+ inherited bases run parent-first, and modules with one drawing method conventionally name it `call`.
79
+ - Breaking: made bracket and call notation the canonical Geometry constructors. Among Data value types, bracket notation
80
+ is public only for Point, Segment, LengthAngle, Margin, and Paper; use `.new` for Location and result carriers, and
81
+ `Stay` for traversal stop tokens.
82
+ - Breaking: aligned Point and Segment comparison with Ruby `Comparable`; malformed or unrelated comparison operands now
83
+ return nil from `<=>`.
84
+ - Breaking: executor entrypoints now return immutable `Executor::Result` values and expose only `execute` and
85
+ `execute_file`; inspect `result.error`, `result.stack`, and `result.value` instead of executor scopes or lifecycle state.
86
+ - Breaking: wrapper attributes and callable arguments use distinct channels, `With` rejects parentless receivers, and
87
+ unsupported direct constructors are private; use the documented factories for Content, concrete Geometry lined
88
+ elements, and Grid query values.
89
+ - Breaking: Canvas uses `.new` for explicit fields, `.from_paper(paper, **overrides)` for paper conversion, and
90
+ `.call`/`Graphics.canvas` for dispatch; the misleading keyword-only `.from_paper(width:, height:)` form was removed.
91
+ - Breaking: `Save`, `Write`, `PNG`, `PDF`, and Sundries native export normalize successful destinations to expanded
92
+ String paths and create missing parent directories; change-aware SVG writes still return nil when unchanged.
93
+ - Breaking: renamed `Derender.evaluate_file_children` to `evaluate_children_file`; decompiled nodes now own immutable
94
+ public state while parser, strategy, and construction plumbing remain private.
95
+ - Expanded runtime/YARD parity checks and exact contracts for inherited, extended, generated, and dynamic API surfaces,
96
+ forwarded options, error channels, path ownership, whitespace, namespaces, nil behavior, and return values.
97
+
98
+ ### Fixed
99
+
100
+ - Rejected directories from default file discovery while preserving custom locator matchers.
101
+ - Kept `Ancestral` context in non-rendering `-context` metadata instead of leaking it into SVG attributes.
102
+ - Preserved source Canvas units and names when deriving fitted Grid canvases.
103
+ - Prevented stale documentation assets from mixing old layouts with new HTML, and moved sidebar navigation into the
104
+ tablet menu at 1024px and below.
105
+ - Normalized numeric slots owned by path, shape, transform, tile, and Inkscape page helpers to finite SVG number spelling;
106
+ arbitrary user-supplied attributes remain untouched.
107
+ - Made named callable wrapper and symbol ids stable while omitting defaults for anonymous modules, and indexed every
108
+ rendered id value, including false and numeric values, through its serialized string.
109
+ - Preserved signed Geometry constraint directions, rejected invalid sweep/export channels through Sevgi error families,
110
+ and validated raw output paths before expansion or rendering.
111
+ - Made callable module configuration copy-owned and freeze-aware, document subclasses inherit their nearest profile, and
112
+ false executor boot receivers remain explicit rather than defaulting to an internal scope.
113
+ - Made element trees, attributes, identifiers, locator results, document profiles, Derender nodes, shell results, and
114
+ executor results retain owned immutable snapshots where their public contracts promise value semantics.
115
+ - Corrected Shell combined-output separators, nil export density errors, document render-option routing, executor source
116
+ snapshots, selected-node namespace/whitespace documentation, and Standard character-data validation.
117
+
118
+ ### Removed
119
+
120
+ - Removed public access to pluralization tables and the internal SVG save extension; pluralization rules are now deeply
121
+ immutable.
122
+ - Removed public documentation and constant access for command-line implementation modules; the `sevgi` and `igves`
123
+ executables remain unchanged.
124
+ - Removed eager loading and public documentation of private Showcase build/test support; explicit support entrypoints
125
+ now keep the harness under the Showcase namespace.
126
+ - Removed public access to document profile name normalizers; registry operations retain them as private plumbing.
127
+ - Removed the redundant `Margin.margin` constructor; use canonical bracket notation.
128
+ - Removed public executor orchestration, obsolete callable-module hooks, the old public attribute syntax constants, and
129
+ direct construction of abstract or internally wired Content, Element, and Grid query types.
130
+ - Removed accidental public access to abstract Lined factories, internal element-name/export maps, result/location
131
+ bracket constructors, and direct Stop construction.
132
+
8
133
  ## 0.95.0 - 2026-07-11
9
134
 
10
135
  ### Security
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sevgi Showcase
2
2
 
3
- Executable examples, rendered outputs, and documentation-site support.
3
+ Sevgi Showcase packages the executable examples, their SVG output, and support code for the documentation site.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,22 +8,14 @@ Executable examples, rendered outputs, and documentation-site support.
8
8
  gem install sevgi-showcase
9
9
  ```
10
10
 
11
- ## Require
11
+ ## Contents
12
12
 
13
- ```ruby
14
- require "sevgi/showcase"
15
- ```
16
-
17
- ## Example
18
-
19
- ```ruby
20
- suite = Sevgi::Test::Suite.new("srv")
21
- suite.valids.map(&:name)
22
- ```
13
+ The gem is an asset package rather than a runtime API. It contains the executable `.sevgi` examples, their expected
14
+ SVG output, and private build support used by the documentation site.
23
15
 
24
16
  ## Ruby compatibility
25
17
 
26
- Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4.0 and the current development Ruby from `.ruby-version`.
18
+ Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the development Ruby from `.ruby-version`.
27
19
 
28
20
  ## Native prerequisites
29
21
 
@@ -47,7 +39,7 @@ gem install cairo rsvg2 hexapdf
47
39
 
48
40
  ## Links
49
41
 
50
- - Documentation: https://sevgi.roktas.dev
51
- - API documentation: https://www.rubydoc.info/gems/sevgi-showcase
52
- - Source: https://github.com/roktas/sevgi/tree/main/showcase
53
- - Changelog: https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
42
+ - Documentation: <https://sevgi.roktas.dev>
43
+ - API documentation: <https://www.rubydoc.info/gems/sevgi-showcase>
44
+ - Source: <https://github.com/roktas/sevgi/tree/main/showcase>
45
+ - Changelog: <https://github.com/roktas/sevgi/blob/main/CHANGELOG.md>
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../showcase"
4
+
3
5
  module Sevgi
4
6
  module Showcase
5
7
  # Applies dark-theme color mappings to showcase source files.
@@ -60,5 +62,7 @@ module Sevgi
60
62
  end
61
63
  end
62
64
  end
65
+
66
+ private_constant :Dark
63
67
  end
64
68
  end
@@ -1,78 +1,80 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sevgi
4
- # Minitest helpers used by the showcase component.
5
- # @api private
6
- module Test
7
- # Executable showcase script descriptor.
4
+ module Showcase
5
+ # Minitest helpers used by the showcase component.
8
6
  # @api private
9
- class Script
10
- # @return [String] absolute script path
11
- attr_reader :file
7
+ module Test
8
+ # Executable showcase script descriptor.
9
+ # @api private
10
+ class Script
11
+ # @return [String] absolute script path
12
+ attr_reader :file
12
13
 
13
- # Creates a script descriptor.
14
- # @param file [String] executable .sevgi file
15
- # @return [void]
16
- # @raise [Sevgi::ArgumentError] when the file is missing or not executable
17
- def initialize(file) = @file = ::File.expand_path(sanitize(file))
14
+ # Creates a script descriptor.
15
+ # @param file [String] executable .sevgi file
16
+ # @return [void]
17
+ # @raise [Sevgi::ArgumentError] when the file is missing or not executable
18
+ def initialize(file) = @file = ::File.expand_path(sanitize(file))
18
19
 
19
- # Returns the script directory.
20
- # @return [String]
21
- def dir = @dir ||= ::File.dirname(file)
20
+ # Returns the script directory.
21
+ # @return [String]
22
+ def dir = @dir ||= ::File.dirname(file)
22
23
 
23
- # Returns the script basename.
24
- # @return [String]
25
- def file! = ::File.basename(file)
24
+ # Returns the script basename.
25
+ # @return [String]
26
+ def file! = ::File.basename(file)
26
27
 
27
- # Returns the script name without extension.
28
- # @return [String]
29
- def name = @name ||= ::File.basename(file, ".*")
28
+ # Returns the script name without extension.
29
+ # @return [String]
30
+ def name = @name ||= ::File.basename(file, ".*")
30
31
 
31
- # @overload run(*args)
32
- # Runs the script directly.
33
- # @param args [Array<String>] extra command arguments
34
- # @return [Sevgi::Test::Shell::Result]
35
- def run(*) = Shell.run(file, *)
32
+ # @overload run(*args)
33
+ # Runs the script directly.
34
+ # @param args [Array<String>] extra command arguments
35
+ # @return [Sevgi::Showcase::Test::Shell::Result]
36
+ def run(*) = Shell.run(file, *)
36
37
 
37
- # Returns the script suite name.
38
- # @return [String]
39
- def suite = @suite ||= ::File.basename(dir)
38
+ # Returns the script suite name.
39
+ # @return [String]
40
+ def suite = @suite ||= ::File.basename(dir)
40
41
 
41
- # Reports whether the expected SVG output exists.
42
- # @return [Boolean]
43
- def svg? = ::File.exist?(svg)
42
+ # Reports whether the expected SVG output exists.
43
+ # @return [Boolean]
44
+ def svg? = ::File.exist?(svg)
44
45
 
45
- # Returns the expected SVG output path.
46
- # @return [String]
47
- def svg = @svg ||= ::File.expand_path("#{dir}/#{name}.svg")
46
+ # Returns the expected SVG output path.
47
+ # @return [String]
48
+ def svg = @svg ||= ::File.expand_path("#{dir}/#{name}.svg")
48
49
 
49
- # Returns the expected SVG output basename.
50
- # @return [String]
51
- def svg! = ::File.basename(svg)
50
+ # Returns the expected SVG output basename.
51
+ # @return [String]
52
+ def svg! = ::File.basename(svg)
52
53
 
53
- # Returns the optional YAML metadata path.
54
- # @return [String]
55
- def yml = @yml ||= ::File.expand_path("#{dir}/#{name}.yml")
54
+ # Returns the optional YAML metadata path.
55
+ # @return [String]
56
+ def yml = @yml ||= ::File.expand_path("#{dir}/#{name}.yml")
56
57
 
57
- # Returns the optional YAML metadata basename.
58
- # @return [String]
59
- def yml! = ::File.basename(yml)
58
+ # Returns the optional YAML metadata basename.
59
+ # @return [String]
60
+ def yml! = ::File.basename(yml)
60
61
 
61
- # @overload run_passive(*args)
62
- # Runs the script without writing Save output to files.
63
- # @param args [Array<String>] extra command arguments
64
- # @return [Sevgi::Test::Shell::Result]
65
- def run_passive(*)
66
- warn("...#{name}")
67
- Shell.run("sevgi", "-r", "sevgi/showcase/passive", file, *)
68
- end
62
+ # @overload run_passive(*args)
63
+ # Runs the script without writing Save output to files.
64
+ # @param args [Array<String>] extra command arguments
65
+ # @return [Sevgi::Showcase::Test::Shell::Result]
66
+ def run_passive(*)
67
+ warn("...#{name}")
68
+ Shell.run("sevgi", "-r", "sevgi/showcase/passive", file, *)
69
+ end
69
70
 
70
- private
71
+ private
71
72
 
72
- def sanitize(file)
73
- file.tap do
74
- ArgumentError.("No such file: #{file}") unless ::File.exist?(file)
75
- ArgumentError.("Not an executable: #{file}") unless ::File.executable?(file)
73
+ def sanitize(file)
74
+ file.tap do
75
+ ArgumentError.("No such file: #{file}") unless ::File.exist?(file)
76
+ ArgumentError.("Not an executable: #{file}") unless ::File.executable?(file)
77
+ end
76
78
  end
77
79
  end
78
80
  end
@@ -4,163 +4,167 @@ require "open3"
4
4
  require "sevgi/function"
5
5
 
6
6
  module Sevgi
7
- # Minitest helpers used by the showcase component.
8
- # @api private
9
- module Test
10
- # Shell runner helpers for showcase tests.
7
+ module Showcase
8
+ # Minitest helpers used by the showcase component.
11
9
  # @api private
12
- module Shell
13
- # Shared process-global SIGINT state.
10
+ module Test
11
+ # Shell runner helpers for showcase tests.
14
12
  # @api private
15
- Signals = ::Sevgi::Function::Shell.const_get(:Signals, false)
13
+ module Shell
14
+ # Shared process-global SIGINT state.
15
+ # @api private
16
+ Signals = ::Sevgi::Function::Shell.const_get(:Signals, false)
16
17
 
17
- private_constant :Signals
18
+ private_constant :Signals
18
19
 
19
- # Shell command result.
20
- # @api private
21
- Result = Data.define(:args, :out, :err, :exit_code) do
22
- # Returns the command string.
23
- # @return [String]
24
- def cmd = args.join(" ")
25
-
26
- # Reports whether the command failed.
27
- # @return [Boolean]
28
- def notok? = !ok?
29
-
30
- # Reports whether the command exited successfully.
31
- # @return [Boolean]
32
- def ok? = exit_code&.zero?
33
-
34
- # Returns the first stdout line.
35
- # @return [String, nil]
36
- def outline = out.first
37
-
38
- # Returns stdout as a newline-joined string.
39
- # @return [String]
40
- def to_s = out.join("\n")
41
- end
20
+ # Shell command result.
21
+ # @api private
22
+ Result = Data.define(:args, :out, :err, :exit_code) do
23
+ private_class_method :[]
42
24
 
43
- # Adapted to popen3 from github.com/mina-deploy/mina
44
- # Shell command runner.
45
- # @api private
46
- class Runner
47
- # Creates a runner.
48
- # @return [void]
49
- def initialize
50
- @coathooks = 0
51
- end
25
+ # Returns the command string.
26
+ # @return [String]
27
+ def cmd = args.join(" ")
52
28
 
53
- # Runs a command and captures stdout, stderr, and exit status.
54
- # @param args [Array<String>] command and arguments
55
- # @yield optional stdin writer evaluated with stdin as receiver
56
- # @yieldreturn [Object]
57
- # @return [Sevgi::Test::Shell::Result]
58
- # @raise [StandardError] when the input block raises; the child is terminated and reaped before propagation
59
- # @note The first SIGINT sends TERM and the second sends KILL outside trap context. The handler is restored after
60
- # the last active run.
61
- def run(*args, &block)
62
- @coathooks = 0
63
- out, err, status = Open3.popen3(*args) do |stdin, stdout, stderr, thread|
64
- capture(stdin, stdout, stderr, thread, &block)
65
- end
29
+ # Reports whether the command failed.
30
+ # @return [Boolean]
31
+ def notok? = !ok?
32
+
33
+ # Reports whether the command exited successfully.
34
+ # @return [Boolean]
35
+ def ok? = exit_code&.zero?
66
36
 
67
- Result[args, out, err, status.exitstatus]
37
+ # Returns the first stdout line.
38
+ # @return [String, nil]
39
+ def outline = out.first
40
+
41
+ # Returns stdout as a newline-joined string.
42
+ # @return [String]
43
+ def to_s = out.join("\n")
68
44
  end
69
45
 
70
- private
46
+ # Adapted to popen3 from github.com/mina-deploy/mina
47
+ # Shell command runner.
48
+ # @api private
49
+ class Runner
50
+ # Creates a runner.
51
+ # @return [void]
52
+ def initialize
53
+ @coathooks = 0
54
+ end
71
55
 
72
- # rubocop:disable Lint/RescueException
73
- def capture(stdin, stdout, stderr, thread, &block)
74
- register_capture(thread)
75
- registered = true
76
- readers = outputs(stdout, stderr)
56
+ # Runs a command and captures stdout, stderr, and exit status.
57
+ # @param args [Array<String>] command and arguments
58
+ # @yield optional stdin writer evaluated with stdin as receiver
59
+ # @yieldreturn [Object]
60
+ # @return [Sevgi::Showcase::Test::Shell::Result]
61
+ # @raise [StandardError] when the input block raises; the child is terminated and reaped before propagation
62
+ # @note The first SIGINT sends TERM and the second sends KILL outside trap context. The handler is restored after
63
+ # the last active run.
64
+ def run(*args, &block)
65
+ @coathooks = 0
66
+ out, err, status = Open3.popen3(*args) do |stdin, stdout, stderr, thread|
67
+ capture(stdin, stdout, stderr, thread, &block)
68
+ end
69
+
70
+ Result.new(args:, out:, err:, exit_code: status.exitstatus)
71
+ end
77
72
 
78
- collect_capture(stdin, thread, readers, &block)
79
- rescue Exception
80
- cleanup_failed_capture(stdin, thread, readers)
81
- raise
82
- ensure
83
- finish_capture(stdin, registered)
84
- end
85
- # rubocop:enable Lint/RescueException
73
+ private
86
74
 
87
- def finish_capture(stdin, registered)
88
- close_input(stdin)
89
- Signals.unregister(self) if registered
90
- end
75
+ # rubocop:disable Lint/RescueException
76
+ def capture(stdin, stdout, stderr, thread, &block)
77
+ register_capture(thread)
78
+ registered = true
79
+ readers = outputs(stdout, stderr)
91
80
 
92
- def register_capture(thread)
93
- Signals.register(self, thread.pid)
94
- end
81
+ collect_capture(stdin, thread, readers, &block)
82
+ rescue Exception
83
+ cleanup_failed_capture(stdin, thread, readers)
84
+ raise
85
+ ensure
86
+ finish_capture(stdin, registered)
87
+ end
88
+ # rubocop:enable Lint/RescueException
95
89
 
96
- def cleanup_failed_capture(stdin, thread, readers)
97
- close_input(stdin)
98
- stop_process(thread)
99
- Array(readers).each(&:join)
100
- end
90
+ def finish_capture(stdin, registered)
91
+ close_input(stdin)
92
+ Signals.unregister(self) if registered
93
+ end
101
94
 
102
- def close_input(stdin)
103
- stdin.close unless stdin.closed?
104
- rescue IOError
105
- nil
106
- end
95
+ def register_capture(thread)
96
+ Signals.register(self, thread.pid)
97
+ end
107
98
 
108
- def collect_capture(stdin, thread, readers, &block)
109
- inputs(stdin, thread, &block)
110
- close_input(stdin)
99
+ def cleanup_failed_capture(stdin, thread, readers)
100
+ close_input(stdin)
101
+ stop_process(thread)
102
+ Array(readers).each(&:join)
103
+ end
111
104
 
112
- [readers[0].value, readers[1].value, thread.value]
113
- end
105
+ def close_input(stdin)
106
+ stdin.close unless stdin.closed?
107
+ rescue IOError
108
+ nil
109
+ end
114
110
 
115
- def handle_sigint(pid)
116
- @coathooks += 1
117
- message, signal = if @coathooks > 1
118
- ["SIGINT received again. Force quitting...", "KILL"]
119
- else
120
- ["SIGINT received.", "TERM"]
111
+ def collect_capture(stdin, thread, readers, &block)
112
+ inputs(stdin, thread, &block)
113
+ close_input(stdin)
114
+
115
+ [readers[0].value, readers[1].value, thread.value]
121
116
  end
122
117
 
123
- warn("\n#{message}")
124
- ::Process.kill(signal, pid)
125
- rescue Errno::ESRCH
126
- warn("No process to kill.")
127
- end
118
+ def handle_sigint(pid)
119
+ @coathooks += 1
120
+ message, signal = if @coathooks > 1
121
+ ["SIGINT received again. Force quitting...", "KILL"]
122
+ else
123
+ ["SIGINT received.", "TERM"]
124
+ end
125
+
126
+ warn("\n#{message}")
127
+ ::Process.kill(signal, pid)
128
+ rescue Errno::ESRCH
129
+ warn("No process to kill.")
130
+ end
128
131
 
129
- def inputs(stdin, thread, &block)
130
- stdin.instance_exec(thread, &block) if block
131
- end
132
+ def inputs(stdin, thread, &block)
133
+ stdin.instance_exec(thread, &block) if block
134
+ end
132
135
 
133
- def kill_process(signal, pid)
134
- ::Process.kill(signal, pid)
135
- rescue Errno::ESRCH
136
- nil
137
- end
136
+ def kill_process(signal, pid)
137
+ ::Process.kill(signal, pid)
138
+ rescue Errno::ESRCH
139
+ nil
140
+ end
138
141
 
139
- def outputs(stdout, stderr)
140
- [
141
- Thread.new { stdout.readlines.map(&:chomp) },
142
- Thread.new { stderr.readlines.map(&:chomp) }
143
- ]
144
- end
142
+ def outputs(stdout, stderr)
143
+ [
144
+ Thread.new { stdout.readlines.map(&:chomp) },
145
+ Thread.new { stderr.readlines.map(&:chomp) }
146
+ ]
147
+ end
145
148
 
146
- def stop_process(thread)
147
- return unless thread&.alive?
149
+ def stop_process(thread)
150
+ return unless thread&.alive?
148
151
 
149
- kill_process("TERM", thread.pid)
150
- return if thread.join(1)
152
+ kill_process("TERM", thread.pid)
153
+ return if thread.join(1)
151
154
 
152
- kill_process("KILL", thread.pid)
153
- thread.join
155
+ kill_process("KILL", thread.pid)
156
+ thread.join
157
+ end
154
158
  end
155
- end
156
159
 
157
- extend self
160
+ extend self
158
161
 
159
- # @overload run(*args)
160
- # Runs a command through a fresh runner.
161
- # @param args [Array<String>] command and arguments
162
- # @return [Sevgi::Test::Shell::Result]
163
- def run(...) = Runner.new.run(...)
162
+ # @overload run(*args)
163
+ # Runs a command through a fresh runner.
164
+ # @param args [Array<String>] command and arguments
165
+ # @return [Sevgi::Showcase::Test::Shell::Result]
166
+ def run(...) = Runner.new.run(...)
167
+ end
164
168
  end
165
169
  end
166
170
  end