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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +22 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +264 -0
- data/Rakefile +19 -0
- data/docs/README.md +25 -0
- data/docs/adr/0001-bind-libhegel-through-fiddle.md +54 -0
- data/docs/adr/0002-ship-one-prebuilt-engine-per-platform-specific-gem.md +48 -0
- data/docs/adr/0003-publish-as-hegeltest-require-as-hegel.md +39 -0
- data/docs/adr/0004-expose-generators-through-a-mixin-with-keyword-options.md +42 -0
- data/docs/adr/0005-name-drawn-values-from-the-callers-source-with-prism.md +40 -0
- data/docs/adr/0006-verify-the-binding-in-seven-layers-with-full-coverage.md +51 -0
- data/docs/adr/0007-ship-a-thin-ruby-skill-shaped-for-donation.md +56 -0
- data/docs/adr/0008-revisit-the-binding-after-milestone-c-on-measurement.md +81 -0
- data/docs/adr/0009-turn-the-example-database-on-with-a-key.md +89 -0
- data/docs/adr/0010-declare-stateful-rules-with-a-class-macro.md +113 -0
- data/docs/adr/0011-let-the-test-case-own-every-pool-drawn-from-it.md +83 -0
- data/docs/adr/0012-build-a-failure-origin-from-the-callers-own-frame.md +72 -0
- data/docs/adr/0013-bind-libhegel-through-the-ffi-gem.md +102 -0
- data/docs/architecture.md +182 -0
- data/lib/hegel/draw_name.rb +109 -0
- data/lib/hegel/errors.rb +47 -0
- data/lib/hegel/generator.rb +98 -0
- data/lib/hegel/generators.rb +865 -0
- data/lib/hegel/lib_hegel/real.rb +1149 -0
- data/lib/hegel/lib_hegel.rb +269 -0
- data/lib/hegel/libhegel_version.rb +9 -0
- data/lib/hegel/locate.rb +188 -0
- data/lib/hegel/report.rb +87 -0
- data/lib/hegel/runner.rb +464 -0
- data/lib/hegel/settings.rb +164 -0
- data/lib/hegel/state_machine.rb +89 -0
- data/lib/hegel/stateful/pool.rb +111 -0
- data/lib/hegel/stateful.rb +120 -0
- data/lib/hegel/syntax/methods.rb +173 -0
- data/lib/hegel/test_case.rb +523 -0
- data/lib/hegel/version.rb +5 -0
- data/lib/hegel.rb +92 -0
- data/lib/hegeltest.rb +7 -0
- data/lib/tasks/libhegel.rake +112 -0
- data/lib/tasks/platform_gems.rake +111 -0
- data/sig/hegel.rbs +563 -0
- data/skills/hegel-ruby/SKILL.md +30 -0
- data/skills/hegel-ruby/references/ruby/reference.md +1210 -0
- 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
|