specproof 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 +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +283 -0
- data/docs/agent-skills.md +60 -0
- data/docs/benchmark.md +24 -0
- data/docs/constrained-partition-pairs-results.md +44 -0
- data/docs/finite-strings-results.md +75 -0
- data/docs/implementation-status.md +65 -0
- data/docs/input-selection-results.md +123 -0
- data/docs/lint-cleanup-plan.md +12 -0
- data/docs/method-targets-results.md +68 -0
- data/docs/minitest-compatibility.md +33 -0
- data/docs/optional-arguments-plan.md +25 -0
- data/docs/optional-arguments-results.md +45 -0
- data/docs/pairwise-partitions-results.md +53 -0
- data/docs/pairwise-results.md +77 -0
- data/docs/pairwise-suggestions-results.md +104 -0
- data/docs/rbs-implementation-status.md +115 -0
- data/docs/rbs-string-literals-results.md +58 -0
- data/docs/real-project-pilot.md +84 -0
- data/docs/report-format.md +244 -0
- data/examples/age_policy/README.md +39 -0
- data/examples/age_policy/faulty/lib/age_policy.rb +9 -0
- data/examples/age_policy/faulty/test/age_policy_examples_test.rb +17 -0
- data/examples/age_policy/faulty/test/age_policy_test.rb +21 -0
- data/examples/age_policy/lib/age_policy.rb +8 -0
- data/examples/age_policy/run.rb +23 -0
- data/examples/age_policy/specification.rb +8 -0
- data/examples/age_policy/test/age_policy_test.rb +18 -0
- data/examples/constrained_partition_pairs/README.md +40 -0
- data/examples/constrained_partition_pairs/lib/account_eligibility.rb +8 -0
- data/examples/constrained_partition_pairs/requirements.md +11 -0
- data/examples/constrained_partition_pairs/run.rb +22 -0
- data/examples/constrained_partition_pairs/specification.rb +17 -0
- data/examples/constrained_partition_pairs/test_account_eligibility.rb +34 -0
- data/examples/input_selection/README.md +22 -0
- data/examples/input_selection/delivery_policy.rb +11 -0
- data/examples/input_selection/requirements.md +9 -0
- data/examples/input_selection/specification.rb +8 -0
- data/examples/input_selection/test_delivery_policy.rb +38 -0
- data/examples/method_targets/README.md +30 -0
- data/examples/method_targets/lib/delivery_defaults.rb +8 -0
- data/examples/method_targets/lib/delivery_options.rb +12 -0
- data/examples/method_targets/requirements.md +20 -0
- data/examples/method_targets/specification_objects.rb +17 -0
- data/examples/method_targets/specification_strings.rb +15 -0
- data/examples/pairwise/README.md +46 -0
- data/examples/pairwise/constrained_requirements.md +6 -0
- data/examples/pairwise/constrained_specification.rb +11 -0
- data/examples/pairwise/corrected/lib/delivery_options.rb +11 -0
- data/examples/pairwise/corrected/test/delivery_options_test.rb +28 -0
- data/examples/pairwise/faulty/lib/delivery_options.rb +11 -0
- data/examples/pairwise/faulty/test/gap_delivery_options_test.rb +10 -0
- data/examples/pairwise/faulty/test/initial_delivery_options_test.rb +23 -0
- data/examples/pairwise/requirements.md +11 -0
- data/examples/pairwise/run.rb +36 -0
- data/examples/pairwise/specification.rb +9 -0
- data/examples/pairwise/three_boolean/lib/boolean_batch.rb +8 -0
- data/examples/pairwise/three_boolean/test/boolean_batch_test.rb +28 -0
- data/examples/pairwise/three_boolean_specification.rb +10 -0
- data/examples/pairwise_partitions/README.md +34 -0
- data/examples/pairwise_partitions/lib/age_policy.rb +8 -0
- data/examples/pairwise_partitions/requirements.md +8 -0
- data/examples/pairwise_partitions/run.rb +22 -0
- data/examples/pairwise_partitions/specification.rb +13 -0
- data/examples/pairwise_partitions/test_age_policy.rb +29 -0
- data/examples/pairwise_suggestions/README.md +48 -0
- data/examples/pairwise_suggestions/run.rb +55 -0
- data/examples/pairwise_suggestions/three_boolean/lib/suggested_boolean_batch.rb +8 -0
- data/examples/pairwise_suggestions/three_boolean/requirements.md +7 -0
- data/examples/pairwise_suggestions/three_boolean/specification.rb +11 -0
- data/examples/pairwise_suggestions/three_boolean/test/baseline_test.rb +9 -0
- data/examples/pairwise_suggestions/three_boolean/test/complete_test.rb +22 -0
- data/examples/partition_gap.rb +8 -0
- data/examples/rbs_registration/README.md +58 -0
- data/examples/rbs_registration/implementations/age_gt/registration.rb +8 -0
- data/examples/rbs_registration/implementations/age_gte/registration.rb +8 -0
- data/examples/rbs_registration/requirements.md +9 -0
- data/examples/rbs_registration/sig/registration.rbs +3 -0
- data/examples/rbs_registration/specification.rb +10 -0
- data/examples/rbs_registration/test_registration.rb +43 -0
- data/examples/rbs_string_literals/README.md +34 -0
- data/examples/rbs_string_literals/lib/delivery_options.rb +9 -0
- data/examples/rbs_string_literals/requirements.md +11 -0
- data/examples/rbs_string_literals/sig/delivery_options.rbs +3 -0
- data/examples/rbs_string_literals/specification.rb +7 -0
- data/examples/rbs_string_literals/test_delivery_options.rb +46 -0
- data/examples/shipping.rb +12 -0
- data/exe/specproof +7 -0
- data/lib/specproof/checker.rb +144 -0
- data/lib/specproof/cli.rb +292 -0
- data/lib/specproof/collector.rb +304 -0
- data/lib/specproof/combinations.rb +226 -0
- data/lib/specproof/composer.rb +97 -0
- data/lib/specproof/coverage.rb +229 -0
- data/lib/specproof/coverage_report.rb +334 -0
- data/lib/specproof/domains.rb +124 -0
- data/lib/specproof/loader.rb +285 -0
- data/lib/specproof/minitest.rb +443 -0
- data/lib/specproof/model.rb +198 -0
- data/lib/specproof/partition_space.rb +268 -0
- data/lib/specproof/planner.rb +125 -0
- data/lib/specproof/report.rb +248 -0
- data/lib/specproof/skills.rb +188 -0
- data/lib/specproof/sources/rbs.rb +137 -0
- data/lib/specproof/sources/rbs_projector.rb +268 -0
- data/lib/specproof/suggestion_report.rb +159 -0
- data/lib/specproof/suggestions.rb +352 -0
- data/lib/specproof/target_name.rb +100 -0
- data/lib/specproof/type_constraints.rb +76 -0
- data/lib/specproof/version.rb +7 -0
- data/lib/specproof.rb +100 -0
- data/sig/specproof.rbs +222 -0
- data/skills/specproof-specify/SKILL.md +282 -0
- data/skills/specproof-specify/references/usage.md +265 -0
- data/skills/specproof-test-gaps/SKILL.md +214 -0
- data/skills/specproof-test-gaps/references/usage.md +231 -0
- metadata +165 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "minitest/autorun"
|
|
5
|
+
require "specproof/minitest"
|
|
6
|
+
|
|
7
|
+
mode = ARGV.shift || "correct"
|
|
8
|
+
output = ARGV.shift || File.expand_path("../../tmp/age-policy-#{mode}.json", __dir__)
|
|
9
|
+
tests = {
|
|
10
|
+
"correct" => "test/age_policy_test.rb",
|
|
11
|
+
"missing" => "faulty/test/age_policy_examples_test.rb",
|
|
12
|
+
"regression" => "faulty/test/age_policy_test.rb"
|
|
13
|
+
}.freeze
|
|
14
|
+
abort "Choose correct, missing, or regression" unless tests.key?(mode)
|
|
15
|
+
|
|
16
|
+
implementation = mode == "correct" ? "lib/age_policy.rb" : "faulty/lib/age_policy.rb"
|
|
17
|
+
require File.join(__dir__, implementation)
|
|
18
|
+
SpecProof::Minitest.install!(
|
|
19
|
+
specifications: [File.join(__dir__, "specification.rb")],
|
|
20
|
+
output: output,
|
|
21
|
+
fail_on_missing: ENV["SPECPROOF_STRICT"] == "1"
|
|
22
|
+
)
|
|
23
|
+
require File.join(__dir__, tests.fetch(mode))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require_relative "../lib/age_policy"
|
|
5
|
+
|
|
6
|
+
# Verifies the declared age policy examples.
|
|
7
|
+
class AgePolicyTest < Minitest::Test
|
|
8
|
+
def test_adult_boundary
|
|
9
|
+
policy = AgePolicy.new
|
|
10
|
+
|
|
11
|
+
refute policy.adult?(0)
|
|
12
|
+
refute policy.adult?(1)
|
|
13
|
+
refute policy.adult?(17)
|
|
14
|
+
assert policy.adult?(18)
|
|
15
|
+
assert policy.adult?(119)
|
|
16
|
+
assert policy.adult?(120)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Constrained pairwise coverage over integer partitions
|
|
2
|
+
|
|
3
|
+
This example combines the complete, non-overlapping age categories `minor`
|
|
4
|
+
(`0...18`) and `adult` (`18..120`) with finite account and country values. Its
|
|
5
|
+
[independent requirements](requirements.md) prohibit minors from having a
|
|
6
|
+
business account and define eligibility from age alone. The restriction uses
|
|
7
|
+
`in_partition(:minor)` to select the whole integer category:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
combinations :age, :account, :country do
|
|
11
|
+
forbid age: in_partition(:minor), account: "business"
|
|
12
|
+
end
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Run from the `gems/specproof` directory with the prepared Ruby and bundle for
|
|
16
|
+
this checkout. Reports and suggestion output go under `tmp/`:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
bundle exec ruby -Ilib exe/specproof plan examples/constrained_partition_pairs/specification.rb --criterion pairwise-partitions-v1 --format json
|
|
20
|
+
bundle exec ruby examples/constrained_partition_pairs/run.rb initial
|
|
21
|
+
bundle exec ruby -Ilib exe/specproof suggest examples/constrained_partition_pairs/specification.rb --report tmp/constrained-partition-pairs-initial.json --format json > tmp/constrained-partition-pairs-suggestions.json
|
|
22
|
+
bundle exec ruby examples/constrained_partition_pairs/run.rb complete
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The two integer categories and two finite domains produce eight possible full
|
|
26
|
+
assignments. The requirement-backed restriction removes the two assignments
|
|
27
|
+
with a minor and a business account, leaving six allowed tuples. The plan has
|
|
28
|
+
eleven required category-pair obligations and one `excluded_pairs` entry with
|
|
29
|
+
`reason: "no_allowed_completion"`. The initial process passes three requirement-
|
|
30
|
+
backed calls and hits nine obligations. `suggest` proposes the two remaining
|
|
31
|
+
allowed assignments, `age: 18, account: "business", country: "de"` and
|
|
32
|
+
`age: 0, account: "personal", country: "de"`. The complete process adds those
|
|
33
|
+
calls with expected results justified by the separate requirement.
|
|
34
|
+
|
|
35
|
+
The normalized selector value used for the contract digest is
|
|
36
|
+
`{ "kind": "partition_reference", "name": "minor" }`. Planned pair values
|
|
37
|
+
continue to use the existing partition object with its name and inclusive
|
|
38
|
+
bounds. Suggestions choose each partition's lower endpoint as a concrete call
|
|
39
|
+
input. Neither category coverage nor a suggested input establishes boundary
|
|
40
|
+
coverage or an expected result.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Applies the age requirement to supported account and country values.
|
|
4
|
+
class AccountEligibility
|
|
5
|
+
def eligible?(age:, account:, country:)
|
|
6
|
+
%w[personal business].include?(account) && %w[ro de].include?(country) && age >= 18
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Account eligibility requirements
|
|
2
|
+
|
|
3
|
+
An account may be opened only by an adult. Minors cannot have a business
|
|
4
|
+
account. Both supported account types (`"personal"` and `"business"`) and
|
|
5
|
+
countries (`"ro"` and `"de"`) are otherwise valid. Eligibility is true exactly
|
|
6
|
+
when age is at least 18; account type and country do not change that result.
|
|
7
|
+
|
|
8
|
+
The restriction on business accounts justifies forbidding the whole `minor`
|
|
9
|
+
partition with `account: "business"`. The age-based eligibility rule supplies
|
|
10
|
+
expected outputs to the example tests. Suggestions provide inputs only and
|
|
11
|
+
are not an output oracle.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "minitest/autorun"
|
|
5
|
+
require "specproof/minitest"
|
|
6
|
+
require_relative "lib/account_eligibility"
|
|
7
|
+
|
|
8
|
+
mode = ARGV.shift || "initial"
|
|
9
|
+
abort "Choose initial or complete" unless %w[initial complete].include?(mode)
|
|
10
|
+
|
|
11
|
+
ENV["ADD_MISSING_PAIRS"] = mode == "complete" ? "1" : "0"
|
|
12
|
+
root = File.expand_path("../..", __dir__)
|
|
13
|
+
report = File.join(root, "tmp/constrained-partition-pairs-#{mode}.json")
|
|
14
|
+
|
|
15
|
+
SpecProof::Minitest.install!(
|
|
16
|
+
specifications: [File.join(__dir__, "specification.rb")],
|
|
17
|
+
criterion: "pairwise-partitions-v1",
|
|
18
|
+
output: report,
|
|
19
|
+
fail_on_missing: false
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
require_relative "test_account_eligibility"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "specproof"
|
|
4
|
+
|
|
5
|
+
SpecProof.define "AccountEligibility#eligible?" do
|
|
6
|
+
input :age, range: 0..120, allow_gaps: false, allow_overlaps: false do
|
|
7
|
+
partition :minor, range: 0...18
|
|
8
|
+
partition :adult, range: 18..120
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
input :account, values: %w[personal business]
|
|
12
|
+
input :country, values: %w[ro de]
|
|
13
|
+
|
|
14
|
+
combinations :age, :account, :country do
|
|
15
|
+
forbid age: in_partition(:minor), account: "business"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require_relative "lib/account_eligibility"
|
|
5
|
+
|
|
6
|
+
# Covers selected age-category, account, and country pairs.
|
|
7
|
+
class AccountEligibilityPairwiseTest < Minitest::Test
|
|
8
|
+
BASELINE_CASES = [
|
|
9
|
+
[16, "personal", "ro", false],
|
|
10
|
+
[30, "business", "ro", true],
|
|
11
|
+
[30, "personal", "de", true]
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
def test_baseline_calls_follow_the_age_requirement
|
|
15
|
+
policy = AccountEligibility.new
|
|
16
|
+
|
|
17
|
+
BASELINE_CASES.each do |age, account, country, expected|
|
|
18
|
+
assert_equal expected,
|
|
19
|
+
policy.eligible?(age: age, account: account, country: country),
|
|
20
|
+
"requirements.md: eligibility depends on age >= 18, not account or country"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if ENV["ADD_MISSING_PAIRS"] == "1"
|
|
25
|
+
def test_suggested_assignments_follow_the_age_requirement
|
|
26
|
+
policy = AccountEligibility.new
|
|
27
|
+
|
|
28
|
+
assert_equal false, policy.eligible?(age: 0, account: "personal", country: "de"),
|
|
29
|
+
"requirements.md: every minor is ineligible"
|
|
30
|
+
assert_equal true, policy.eligible?(age: 18, account: "business", country: "de"),
|
|
31
|
+
"requirements.md: every adult with a supported account and country is eligible"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Explicit input selection
|
|
2
|
+
|
|
3
|
+
This example analyzes the boolean `expedited` option and explicitly excludes
|
|
4
|
+
the ordinary `Hash` payload. The expected results come from the independent
|
|
5
|
+
[delivery requirements](requirements.md), not from the implementation.
|
|
6
|
+
|
|
7
|
+
From the `gems/specproof` checkout, inspect the offline contract and candidate
|
|
8
|
+
plan, then run the Minitest suite:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
bundle exec ruby -Ilib exe/specproof check examples/input_selection/specification.rb --format json
|
|
12
|
+
bundle exec ruby -Ilib exe/specproof plan examples/input_selection/specification.rb --format json
|
|
13
|
+
bundle exec ruby -Ilib examples/input_selection/test_delivery_policy.rb
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The plan has one candidate for each boolean value. The suite asserts both
|
|
17
|
+
acceptance and rejection under the independent rules and passes ordinary Hash
|
|
18
|
+
values at runtime. Its coverage report concerns only the selected `expedited`
|
|
19
|
+
input. It says nothing about payload values, payload content, output behavior
|
|
20
|
+
outside the assertions, interactions, or complete method correctness. An
|
|
21
|
+
excluded input cannot be used to claim coverage for that input. If a selected
|
|
22
|
+
input receives a non-primitive value, collection is incomplete.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Evaluates whether a delivery meets its option requirements.
|
|
4
|
+
class DeliveryPolicy
|
|
5
|
+
def accept?(payload, expedited:)
|
|
6
|
+
return false unless payload.fetch(:ready)
|
|
7
|
+
return true unless expedited
|
|
8
|
+
|
|
9
|
+
payload.fetch(:priority) == :high
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Delivery option requirements
|
|
2
|
+
|
|
3
|
+
The option decision is evaluated only for ready deliveries. A regular ready
|
|
4
|
+
delivery is accepted regardless of payload details. An expedited ready delivery
|
|
5
|
+
is accepted exactly when its payload has `priority: :high`. A delivery that is
|
|
6
|
+
not ready is rejected.
|
|
7
|
+
|
|
8
|
+
These rules are the expected-result oracle for the example tests. They describe
|
|
9
|
+
behavior independently of `DeliveryPolicy#accept?`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest/autorun"
|
|
4
|
+
require_relative "delivery_policy"
|
|
5
|
+
require "specproof/minitest"
|
|
6
|
+
|
|
7
|
+
SpecProof::Minitest.install!(
|
|
8
|
+
specifications: ["examples/input_selection/specification.rb"],
|
|
9
|
+
output: "tmp/specproof-input-selection-coverage.json",
|
|
10
|
+
fail_on_missing: true
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# Exercises delivery options against the independent requirements.
|
|
14
|
+
class DeliveryPolicyTest < Minitest::Test
|
|
15
|
+
def setup
|
|
16
|
+
@policy = DeliveryPolicy.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_regular_ready_delivery_is_accepted
|
|
20
|
+
assert @policy.accept?({ ready: true, priority: :low }, expedited: false),
|
|
21
|
+
"A regular ready delivery is accepted (requirements.md)"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_high_priority_expedited_delivery_is_accepted
|
|
25
|
+
assert @policy.accept?({ ready: true, priority: :high }, expedited: true),
|
|
26
|
+
"A high-priority expedited delivery is accepted (requirements.md)"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_low_priority_expedited_delivery_is_rejected
|
|
30
|
+
refute @policy.accept?({ ready: true, priority: :low }, expedited: true),
|
|
31
|
+
"A low-priority expedited delivery is rejected (requirements.md)"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_unready_delivery_is_rejected
|
|
35
|
+
refute @policy.accept?({ ready: false, priority: :high }, expedited: false),
|
|
36
|
+
"An unready delivery is rejected (requirements.md)"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# String and method-object targets
|
|
2
|
+
|
|
3
|
+
The requirements in [requirements.md](requirements.md) define independent
|
|
4
|
+
input and behavior rules for an instance method, a class method, and a module
|
|
5
|
+
method. The two declarations below select those same methods in equivalent
|
|
6
|
+
ways and produce the same canonical target identities, contract digest, and
|
|
7
|
+
obligation IDs.
|
|
8
|
+
|
|
9
|
+
Run from the `gems/specproof` directory with the checkout's Ruby and bundle:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bundle exec ruby -Ilib exe/specproof plan examples/method_targets/specification_strings.rb --format json > tmp/method-targets-strings.json
|
|
13
|
+
bundle exec ruby -Ilib exe/specproof plan examples/method_targets/specification_objects.rb --format json > tmp/method-targets-objects.json
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The first command uses strings and does not load or autoload the application
|
|
17
|
+
owners. The second loads the definitions so Ruby can provide one
|
|
18
|
+
`UnboundMethod` and two `Method` objects; SpecProof inspects those objects
|
|
19
|
+
without invoking the methods. Compare `contract_digest` and the typed
|
|
20
|
+
obligation `id`/`target` pairs in the reports. The target fields remain
|
|
21
|
+
canonical strings, such as `DeliveryOptions#build` and
|
|
22
|
+
`DeliveryDefaults.build`.
|
|
23
|
+
|
|
24
|
+
String declarations are a good fit for offline checking or planning when
|
|
25
|
+
application loading is unavailable or could trigger autoload. Method objects
|
|
26
|
+
are useful in runtime declarations after code has loaded. They must refer to
|
|
27
|
+
directly defined public methods on named classes/modules and remain current at
|
|
28
|
+
declaration time. Aliases, inherited singleton methods, anonymous owners, and
|
|
29
|
+
bound methods on arbitrary instances are unsupported. Editor navigation from
|
|
30
|
+
target names depends on language-server support.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Builds delivery options for requested quantities and scheduling modes.
|
|
4
|
+
class DeliveryOptions
|
|
5
|
+
def build(quantity:)
|
|
6
|
+
{ quantity: quantity, package: quantity.zero? ? :empty : :parcel }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.build(mode:)
|
|
10
|
+
{ mode: mode, schedule_at_checkout: mode == :scheduled }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Independent requirements
|
|
2
|
+
|
|
3
|
+
These rules define the example's expected behavior independently of its Ruby
|
|
4
|
+
implementation:
|
|
5
|
+
|
|
6
|
+
- An instance delivery build accepts quantities from `0` through `100`; a
|
|
7
|
+
zero quantity selects the `:empty` package and every positive quantity
|
|
8
|
+
selects `:parcel` in its result.
|
|
9
|
+
- The class-level delivery builder supports `:scheduled` and `:immediate`;
|
|
10
|
+
scheduling at checkout is enabled only for `:scheduled`.
|
|
11
|
+
- The module-level default builder supports `:local` and `:remote`; local
|
|
12
|
+
delivery selects the `:nearby` carrier and remote delivery selects
|
|
13
|
+
`:national`.
|
|
14
|
+
|
|
15
|
+
The two specification files encode the same input domains and canonical target
|
|
16
|
+
names. `specification_strings.rb` can be planned without loading these methods.
|
|
17
|
+
`specification_objects.rb` loads the file and demonstrates equivalent
|
|
18
|
+
`UnboundMethod`/`Method` selections. The runtime methods exist to make those
|
|
19
|
+
objects available; the requirements above remain the source of expected
|
|
20
|
+
behavior.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "specproof"
|
|
4
|
+
require_relative "lib/delivery_options"
|
|
5
|
+
require_relative "lib/delivery_defaults"
|
|
6
|
+
|
|
7
|
+
SpecProof.define DeliveryOptions.instance_method(:build) do
|
|
8
|
+
input :quantity, range: 0..100
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
SpecProof.define DeliveryOptions.method(:build) do
|
|
12
|
+
input :mode, values: %i[immediate scheduled]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
SpecProof.define DeliveryDefaults.method(:build) do
|
|
16
|
+
input :region, values: %i[local remote]
|
|
17
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "specproof"
|
|
4
|
+
|
|
5
|
+
SpecProof.define "DeliveryOptions#build" do
|
|
6
|
+
input :quantity, range: 0..100
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
SpecProof.define "DeliveryOptions.build" do
|
|
10
|
+
input :mode, values: %i[immediate scheduled]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
SpecProof.define "DeliveryDefaults.build" do
|
|
14
|
+
input :region, values: %i[local remote]
|
|
15
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Pairwise input scenarios
|
|
2
|
+
|
|
3
|
+
The [requirements](requirements.md) independently say that express speed is
|
|
4
|
+
selected exactly when `expedited` is true, and the notification flag follows
|
|
5
|
+
`notify`. The input declaration describes finite booleans; it does not encode
|
|
6
|
+
those output rules.
|
|
7
|
+
|
|
8
|
+
Run these commands from the `gems/specproof` directory with its development
|
|
9
|
+
bundle:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bundle exec ruby -Ilib exe/specproof plan examples/pairwise/specification.rb --criterion pairwise-finite-v1 --format json
|
|
13
|
+
bundle exec ruby examples/pairwise/run.rb initial
|
|
14
|
+
bundle exec ruby examples/pairwise/run.rb gap
|
|
15
|
+
bundle exec ruby examples/pairwise/run.rb corrected
|
|
16
|
+
bundle exec ruby -Ilib exe/specproof plan examples/pairwise/constrained_specification.rb --criterion pairwise-finite-v1 --format json
|
|
17
|
+
bundle exec ruby -Ilib exe/specproof plan examples/pairwise/three_boolean_specification.rb --criterion pairwise-finite-v1 --format json
|
|
18
|
+
bundle exec ruby examples/pairwise/run.rb three
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The plan has four pair obligations for the two booleans. The `initial` process
|
|
22
|
+
runs three independent assertions for `00`, `01`, and `10`: all three tests
|
|
23
|
+
pass and coverage is 3/4. The faulty method computes the notification flag as
|
|
24
|
+
`notify && !expedited`; it agrees with the requirement on those three cases.
|
|
25
|
+
The `gap` process repeats them and adds the requirement-backed `11` assertion.
|
|
26
|
+
That assertion fails, although all four pair obligations have now been
|
|
27
|
+
exercised (4/4). The `corrected` process uses a separate implementation and
|
|
28
|
+
passes all four assertions with 4/4 pair coverage. Each mode runs in its own
|
|
29
|
+
process so the implementations never coexist.
|
|
30
|
+
|
|
31
|
+
The constrained declaration has its own [requirement](constrained_requirements.md):
|
|
32
|
+
expedited delivery requires notification. It explicitly forbids
|
|
33
|
+
`expedited: true, notify: false`, so the plan has three required pairs and one
|
|
34
|
+
`excluded_pairs` row with `reason: "no_allowed_completion"`. This rule is not
|
|
35
|
+
used by the unconstrained example and must not be inferred from the faulty
|
|
36
|
+
implementation.
|
|
37
|
+
|
|
38
|
+
The three-boolean declaration has 12 possible pair rows. Four calls, `000`,
|
|
39
|
+
`011`, `101`, and `110`, exercise all 12. That is pair coverage for the three
|
|
40
|
+
input pairs; it does not claim all eight full tuples were run or that all
|
|
41
|
+
application behavior is covered.
|
|
42
|
+
|
|
43
|
+
Each runner mode writes a fresh report below `tmp/pairwise-*.json`. Compare its
|
|
44
|
+
criterion, digest, typed pair rows, run ID, integrity, and `tests_passed` with
|
|
45
|
+
the current plan and command. Coverage records method calls; the assertions
|
|
46
|
+
derive expected values from the separate requirements file.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Constrained delivery requirement
|
|
2
|
+
|
|
3
|
+
For this separate target, expedited delivery requires notification. Therefore
|
|
4
|
+
the full tuple `expedited: true, notify: false` is invalid. The other three
|
|
5
|
+
boolean tuples are allowed. This requirement justifies the explicit forbidden
|
|
6
|
+
assignment in `constrained_specification.rb`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "specproof"
|
|
4
|
+
|
|
5
|
+
SpecProof.define "ConstrainedDelivery#build" do
|
|
6
|
+
input :expedited, values: [false, true]
|
|
7
|
+
input :notify, values: [false, true]
|
|
8
|
+
combinations :expedited, :notify do
|
|
9
|
+
forbid expedited: true, notify: false
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Checks the corrected implementation against each stated delivery rule.
|
|
4
|
+
class CorrectedDeliveryOptionsTest < Minitest::Test
|
|
5
|
+
def setup
|
|
6
|
+
@delivery_options = DeliveryOptions.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def test_standard_delivery_without_notification
|
|
10
|
+
assert_equal({ speed: :standard, notification: false },
|
|
11
|
+
@delivery_options.build(expedited: false, notify: false))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_standard_delivery_with_notification
|
|
15
|
+
assert_equal({ speed: :standard, notification: true },
|
|
16
|
+
@delivery_options.build(expedited: false, notify: true))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_express_delivery_without_notification
|
|
20
|
+
assert_equal({ speed: :express, notification: false },
|
|
21
|
+
@delivery_options.build(expedited: true, notify: false))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_express_delivery_with_notification
|
|
25
|
+
assert_equal({ speed: :express, notification: true },
|
|
26
|
+
@delivery_options.build(expedited: true, notify: true))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Builds the delivery choices with the documented interaction defect.
|
|
4
|
+
class DeliveryOptions
|
|
5
|
+
def build(expedited:, notify:)
|
|
6
|
+
{
|
|
7
|
+
speed: expedited ? :express : :standard,
|
|
8
|
+
notification: notify && !expedited
|
|
9
|
+
}
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adds the requirement-backed pair missing from the baseline tests.
|
|
4
|
+
class GapDeliveryOptionsTest < Minitest::Test
|
|
5
|
+
def test_express_delivery_with_notification
|
|
6
|
+
assert_equal({ speed: :express, notification: true },
|
|
7
|
+
DeliveryOptions.new.build(expedited: true, notify: true),
|
|
8
|
+
"Requirement: notification follows notify; see requirements.md")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Covers the three baseline option combinations.
|
|
4
|
+
class InitialDeliveryOptionsTest < Minitest::Test
|
|
5
|
+
def setup
|
|
6
|
+
@delivery_options = DeliveryOptions.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def test_standard_delivery_without_notification
|
|
10
|
+
assert_equal({ speed: :standard, notification: false },
|
|
11
|
+
@delivery_options.build(expedited: false, notify: false))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_standard_delivery_with_notification
|
|
15
|
+
assert_equal({ speed: :standard, notification: true },
|
|
16
|
+
@delivery_options.build(expedited: false, notify: true))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_express_delivery_without_notification
|
|
20
|
+
assert_equal({ speed: :express, notification: false },
|
|
21
|
+
@delivery_options.build(expedited: true, notify: false))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Delivery option requirements
|
|
2
|
+
|
|
3
|
+
These are the normative requirements for `DeliveryOptions#build`:
|
|
4
|
+
|
|
5
|
+
1. Speed is `:express` exactly when `expedited` is true; otherwise it is
|
|
6
|
+
`:standard`.
|
|
7
|
+
2. The notification flag equals `notify`, independent of the speed selection.
|
|
8
|
+
|
|
9
|
+
Both inputs are booleans. Every combination is valid for this example. The
|
|
10
|
+
faulty and corrected implementations are judged against these rules, not used
|
|
11
|
+
to derive them.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "minitest/autorun"
|
|
5
|
+
require "specproof/minitest"
|
|
6
|
+
|
|
7
|
+
mode = ARGV.shift || "corrected"
|
|
8
|
+
output = ARGV.shift || File.expand_path("../../tmp/pairwise-#{mode}.json", __dir__)
|
|
9
|
+
implementations = {
|
|
10
|
+
"initial" => ["faulty/lib/delivery_options.rb"],
|
|
11
|
+
"gap" => ["faulty/lib/delivery_options.rb"],
|
|
12
|
+
"corrected" => ["corrected/lib/delivery_options.rb"],
|
|
13
|
+
"three" => ["three_boolean/lib/boolean_batch.rb"]
|
|
14
|
+
}.freeze
|
|
15
|
+
test_files = {
|
|
16
|
+
"initial" => ["faulty/test/initial_delivery_options_test.rb"],
|
|
17
|
+
"gap" => ["faulty/test/initial_delivery_options_test.rb", "faulty/test/gap_delivery_options_test.rb"],
|
|
18
|
+
"corrected" => ["corrected/test/delivery_options_test.rb"],
|
|
19
|
+
"three" => ["three_boolean/test/boolean_batch_test.rb"]
|
|
20
|
+
}.freeze
|
|
21
|
+
specifications = {
|
|
22
|
+
"initial" => "specification.rb",
|
|
23
|
+
"gap" => "specification.rb",
|
|
24
|
+
"corrected" => "specification.rb",
|
|
25
|
+
"three" => "three_boolean_specification.rb"
|
|
26
|
+
}.freeze
|
|
27
|
+
abort "Choose initial, gap, corrected, or three" unless implementations.key?(mode)
|
|
28
|
+
|
|
29
|
+
require File.join(__dir__, implementations.fetch(mode).first)
|
|
30
|
+
SpecProof::Minitest.install!(
|
|
31
|
+
specifications: [File.join(__dir__, specifications.fetch(mode))],
|
|
32
|
+
criterion: "pairwise-finite-v1",
|
|
33
|
+
output: output,
|
|
34
|
+
fail_on_missing: false
|
|
35
|
+
)
|
|
36
|
+
test_files.fetch(mode).each { |path| require File.join(__dir__, path) }
|