hegeltest 0.1.0

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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +22 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +264 -0
  6. data/Rakefile +19 -0
  7. data/docs/README.md +25 -0
  8. data/docs/adr/0001-bind-libhegel-through-fiddle.md +54 -0
  9. data/docs/adr/0002-ship-one-prebuilt-engine-per-platform-specific-gem.md +48 -0
  10. data/docs/adr/0003-publish-as-hegeltest-require-as-hegel.md +39 -0
  11. data/docs/adr/0004-expose-generators-through-a-mixin-with-keyword-options.md +42 -0
  12. data/docs/adr/0005-name-drawn-values-from-the-callers-source-with-prism.md +40 -0
  13. data/docs/adr/0006-verify-the-binding-in-seven-layers-with-full-coverage.md +51 -0
  14. data/docs/adr/0007-ship-a-thin-ruby-skill-shaped-for-donation.md +56 -0
  15. data/docs/adr/0008-revisit-the-binding-after-milestone-c-on-measurement.md +81 -0
  16. data/docs/adr/0009-turn-the-example-database-on-with-a-key.md +89 -0
  17. data/docs/adr/0010-declare-stateful-rules-with-a-class-macro.md +113 -0
  18. data/docs/adr/0011-let-the-test-case-own-every-pool-drawn-from-it.md +83 -0
  19. data/docs/adr/0012-build-a-failure-origin-from-the-callers-own-frame.md +72 -0
  20. data/docs/adr/0013-bind-libhegel-through-the-ffi-gem.md +102 -0
  21. data/docs/architecture.md +182 -0
  22. data/lib/hegel/draw_name.rb +109 -0
  23. data/lib/hegel/errors.rb +47 -0
  24. data/lib/hegel/generator.rb +98 -0
  25. data/lib/hegel/generators.rb +865 -0
  26. data/lib/hegel/lib_hegel/real.rb +1149 -0
  27. data/lib/hegel/lib_hegel.rb +269 -0
  28. data/lib/hegel/libhegel_version.rb +9 -0
  29. data/lib/hegel/locate.rb +188 -0
  30. data/lib/hegel/report.rb +87 -0
  31. data/lib/hegel/runner.rb +464 -0
  32. data/lib/hegel/settings.rb +164 -0
  33. data/lib/hegel/state_machine.rb +89 -0
  34. data/lib/hegel/stateful/pool.rb +111 -0
  35. data/lib/hegel/stateful.rb +120 -0
  36. data/lib/hegel/syntax/methods.rb +173 -0
  37. data/lib/hegel/test_case.rb +523 -0
  38. data/lib/hegel/version.rb +5 -0
  39. data/lib/hegel.rb +92 -0
  40. data/lib/hegeltest.rb +7 -0
  41. data/lib/tasks/libhegel.rake +112 -0
  42. data/lib/tasks/platform_gems.rake +111 -0
  43. data/sig/hegel.rbs +563 -0
  44. data/skills/hegel-ruby/SKILL.md +30 -0
  45. data/skills/hegel-ruby/references/ruby/reference.md +1210 -0
  46. metadata +113 -0
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "net/http"
6
+ require_relative "../hegel/locate"
7
+ require_relative "../hegel/libhegel_version"
8
+
9
+ # Downloads the pinned libhegel release asset for the host platform into
10
+ # tmp/libhegel/<version>/, verifying it against the published SHA-256 before
11
+ # installing it. This is the one place in the codebase allowed to touch the
12
+ # network (see lib/hegel/locate.rb's module comment); everything a test run
13
+ # needs is fetched here ahead of time, not at resolution time.
14
+ module Hegel
15
+ module LibhegelFetch
16
+ RELEASE_BASE = "https://github.com/hegeldev/hegel-rust/releases/download"
17
+ DEFAULT_ROOT = File.expand_path("../../tmp/libhegel", __dir__)
18
+
19
+ module_function
20
+
21
+ # Fetches the host's pinned asset into <root>/<version>/<asset>,
22
+ # skipping the download if it is already there. `downloader` is
23
+ # injectable so the orchestration (skip-if-present, checksum parsing,
24
+ # mismatch handling) is testable without the network; it defaults to a
25
+ # real HTTP GET.
26
+ def fetch_host_asset(version: Hegel::LIBHEGEL_VERSION, host_cpu: RbConfig::CONFIG["host_cpu"],
27
+ host_os: RbConfig::CONFIG["host_os"], root: DEFAULT_ROOT, downloader: method(:http_get))
28
+ asset = Hegel::Locate.asset_name(host_cpu: host_cpu, host_os: host_os)
29
+ dest = File.join(root, version, asset)
30
+ return dest if File.file?(dest)
31
+
32
+ base = "#{RELEASE_BASE}/v#{version}"
33
+ bytes = downloader.call("#{base}/#{asset}")
34
+ checksum_line = downloader.call("#{base}/#{asset}.sha256")
35
+ verify_and_install(bytes, expected_sha256(checksum_line), dest)
36
+ end
37
+
38
+ # The published checksum file is one line, "<hex> <filename>"; the hex
39
+ # digest is its first whitespace-separated token.
40
+ def expected_sha256(checksum_line)
41
+ checksum_line.split.first
42
+ end
43
+
44
+ # Verifies `bytes` against `expected_hex` and, only on a match, writes
45
+ # them to `dest` (via a same-directory temp file, renamed into place so
46
+ # a reader never sees a partial file). On mismatch, raises without ever
47
+ # touching disk, so a failed fetch never leaves a corrupt file behind.
48
+ def verify_and_install(bytes, expected_hex, dest)
49
+ actual_hex = Digest::SHA256.hexdigest(bytes)
50
+ if actual_hex != expected_hex
51
+ raise Hegel::Error, "SHA-256 mismatch for #{File.basename(dest)}: expected #{expected_hex}, got #{actual_hex}"
52
+ end
53
+
54
+ FileUtils.mkdir_p(File.dirname(dest))
55
+ tmp = "#{dest}.#{Process.pid}.partial"
56
+ File.binwrite(tmp, bytes)
57
+ File.rename(tmp, dest)
58
+ dest
59
+ end
60
+
61
+ # Fetches every published platform's asset (see Hegel::Locate::ASSET_NAMES),
62
+ # not just the host's, into <root>/<version>/<asset>: this is what
63
+ # `rake libhegel:fetch_all` stages the five platform gems from. Repeats
64
+ # fetch_host_asset's steps per asset rather than delegating to it,
65
+ # because delegating would mean computing a host_cpu/host_os pair for
66
+ # each published platform just to have asset_name recompute the asset
67
+ # name it already is.
68
+ def fetch_all_assets(version: Hegel::LIBHEGEL_VERSION, root: DEFAULT_ROOT, downloader: method(:http_get))
69
+ base = "#{RELEASE_BASE}/v#{version}"
70
+ Hegel::Locate::ASSET_NAMES.values.map do |asset|
71
+ dest = File.join(root, version, asset)
72
+ next dest if File.file?(dest)
73
+
74
+ bytes = downloader.call("#{base}/#{asset}")
75
+ checksum_line = downloader.call("#{base}/#{asset}.sha256")
76
+ verify_and_install(bytes, expected_sha256(checksum_line), dest)
77
+ end
78
+ end
79
+
80
+ # Minimal redirect-following GET: GitHub release assets serve a 302 to
81
+ # the actual blob storage host. `getter` is injectable (defaulting to a
82
+ # real Net::HTTP call) so the redirect/success/error branches are
83
+ # testable with hand-built responses, no network involved.
84
+ def http_get(url, redirects: 5, getter: Net::HTTP.method(:get_response))
85
+ response = getter.call(URI(url))
86
+ case response
87
+ when Net::HTTPRedirection
88
+ raise Hegel::Error, "too many redirects for #{url}" if redirects <= 0
89
+
90
+ http_get(response["location"], redirects: redirects - 1, getter: getter)
91
+ when Net::HTTPSuccess
92
+ response.body
93
+ else
94
+ raise Hegel::Error, "HTTP #{response.code} for #{url}"
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ namespace :libhegel do
101
+ desc "Download the pinned libhegel build for this host into tmp/libhegel/<version>/"
102
+ task :fetch do
103
+ dest = Hegel::LibhegelFetch.fetch_host_asset
104
+ puts "libhegel: #{dest}"
105
+ end
106
+
107
+ desc "Download the pinned libhegel build for every published platform into tmp/libhegel/<version>/, " \
108
+ "for `rake platform_gems:build` to package"
109
+ task :fetch_all do
110
+ Hegel::LibhegelFetch.fetch_all_assets.each { |dest| puts "libhegel: #{dest}" }
111
+ end
112
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "tmpdir"
5
+ require "rubygems/package"
6
+ require_relative "../hegel/locate"
7
+ require_relative "../hegel/libhegel_version"
8
+ require_relative "../hegel/errors"
9
+
10
+ # Packages this gem for the five platforms hegel-rust publishes a libhegel
11
+ # build for (see Hegel::Locate::ASSET_NAMES), on top of the platform-
12
+ # independent "ruby" gem `rake build` already produces. A platform gem
13
+ # differs from that ruby gem in exactly two ways: spec.platform names the
14
+ # platform, and lib/hegel/libhegel/ (Hegel::Locate::GEM_LIBHEGEL_DIR) carries
15
+ # that platform's asset plus libhegel's own MIT notice.
16
+ #
17
+ # Building happens in a temporary staging directory copied from the working
18
+ # tree, rather than in the working tree itself. Gem::Package reads every
19
+ # spec.files entry relative to the process's current directory, and staging
20
+ # keeps a five-platform build from ever writing a binary into the repository.
21
+ #
22
+ # This file does not reference Hegel::LibhegelFetch (lib/tasks/libhegel.rake):
23
+ # it redeclares the tmp/libhegel/<version>/<asset> layout as its own
24
+ # ASSET_ROOT so it loads and tests on its own, the same way libhegel.rake
25
+ # does not depend on this file existing.
26
+ module Hegel
27
+ module PlatformGems
28
+ ROOT = File.expand_path("../..", __dir__)
29
+ GEMSPEC_PATH = File.join(ROOT, "hegeltest.gemspec")
30
+ NOTICE_PATH = File.join(ROOT, "NOTICE-libhegel.txt")
31
+ DEFAULT_OUTPUT_DIR = File.join(ROOT, "pkg")
32
+ ASSET_ROOT = File.join(ROOT, "tmp", "libhegel")
33
+
34
+ # Where a platform gem carries its asset and notice, relative to the gem
35
+ # root. Matches Hegel::Locate::GEM_LIBHEGEL_DIR, the directory Locate
36
+ # resolves the bundled copy from once the gem is installed.
37
+ BUNDLE_DIR = "lib/hegel/libhegel"
38
+
39
+ module_function
40
+
41
+ # The gemspec, loaded fresh each call: Gem::Specification.load caches by
42
+ # path, so a caller who wants today's `git ls-files` (after adding a new
43
+ # file, say) must not memoize this across process lifetimes on their own.
44
+ def base_spec
45
+ Gem::Specification.load(GEMSPEC_PATH)
46
+ end
47
+
48
+ # The asset `rake libhegel:fetch_all` (or fetch_host_asset, for the
49
+ # host's own platform) already staged for `platform`, or raises if it has
50
+ # not run yet.
51
+ def asset_path(platform, version: Hegel::LIBHEGEL_VERSION, root: ASSET_ROOT)
52
+ asset = Hegel::Locate::ASSET_NAMES.fetch(platform)
53
+ path = File.join(root, version, asset)
54
+ return path if File.file?(path)
55
+
56
+ raise Hegel::Error, "no fetched libhegel asset for #{platform} at #{path}. Run `rake libhegel:fetch_all` first."
57
+ end
58
+
59
+ # Builds one platform's gem into `output_dir`, from `spec`'s file set plus
60
+ # that platform's asset and the libhegel notice, and returns the path to
61
+ # the built .gem. `spec`, `asset`, and `notice` are injectable so a test
62
+ # can verify the package's contents with fake files, without touching the
63
+ # real working tree or the network.
64
+ def build(spec:, platform:, asset:, notice: NOTICE_PATH, output_dir: DEFAULT_OUTPUT_DIR)
65
+ platform_spec = spec.dup
66
+ platform_spec.platform = Gem::Platform.new(platform)
67
+
68
+ extra_files = {
69
+ "#{BUNDLE_DIR}/#{File.basename(asset)}" => asset,
70
+ "#{BUNDLE_DIR}/#{File.basename(notice)}" => notice
71
+ }
72
+ platform_spec.files = spec.files + extra_files.keys
73
+
74
+ FileUtils.mkdir_p(output_dir)
75
+ gem_path = File.join(output_dir, "#{platform_spec.full_name}.gem")
76
+
77
+ Dir.mktmpdir do |stage|
78
+ spec.files.each { |relative| stage_copy(stage, relative, File.join(ROOT, relative)) }
79
+ extra_files.each { |relative, source| stage_copy(stage, relative, source) }
80
+
81
+ Dir.chdir(stage) { Gem::Package.build(platform_spec, false, false, gem_path) }
82
+ end
83
+
84
+ gem_path
85
+ end
86
+
87
+ def stage_copy(stage, relative, source)
88
+ dest = File.join(stage, relative)
89
+ FileUtils.mkdir_p(File.dirname(dest))
90
+ FileUtils.cp(source, dest)
91
+ end
92
+
93
+ # Builds every published platform's gem, each from an asset
94
+ # `rake libhegel:fetch_all` already staged under `root`.
95
+ def build_all(spec: base_spec, version: Hegel::LIBHEGEL_VERSION, root: ASSET_ROOT, notice: NOTICE_PATH,
96
+ output_dir: DEFAULT_OUTPUT_DIR)
97
+ Hegel::Locate::ASSET_NAMES.each_key.map do |platform|
98
+ build(spec: spec, platform: platform, asset: asset_path(platform, version: version, root: root),
99
+ notice: notice, output_dir: output_dir)
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ namespace :platform_gems do
106
+ desc "Build the hegeltest gem for every platform hegel-rust publishes libhegel for, " \
107
+ "from assets already fetched by `rake libhegel:fetch_all`"
108
+ task :build do
109
+ Hegel::PlatformGems.build_all.each { |path| puts "built: #{path}" }
110
+ end
111
+ end