modspec 0.2.0 → 0.2.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -0
- data/.rubocop_todo.yml +8 -35
- data/lib/modspec/child_container.rb +36 -0
- data/lib/modspec/conformance_class.rb +7 -23
- data/lib/modspec/conformance_test.rb +0 -1
- data/lib/modspec/identifier.rb +0 -1
- data/lib/modspec/normative_statement.rb +2 -38
- data/lib/modspec/normative_statements_class.rb +10 -26
- data/lib/modspec/suite.rb +78 -109
- data/lib/modspec/version.rb +1 -1
- data/lib/modspec.rb +8 -10
- data/spec/modspec/conformance_class_spec.rb +19 -12
- data/spec/modspec/conformance_test_spec.rb +21 -3
- data/spec/modspec/normative_statements_class_spec.rb +16 -4
- data/spec/modspec/suite_spec.rb +404 -19
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ca0254068b698f935d90fbfb963362136ae975df3f2dea02d946bd38b53d07b
|
|
4
|
+
data.tar.gz: 0f24f91cbfdd1674e39a1b37a2cd7ac736771d2edefbfc7930dac858a17d03f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a467d5f5cf70c4df7f20e1b17960f9fc169cd793bfab169f639fee73a4a9838af1f4b6778c7b1e045546ccd3a87fb07f0c3f7a8d4427ea8405acf5aba1c63b4
|
|
7
|
+
data.tar.gz: a2b9ba6b8545bcb5cf7af9a8118f079d3758be7a4342d2aab7342078962308967cfed9b8eb139b86e5ee84aa9f12e9d11a2799b719a12bdb82b3920f06f0b57d
|
data/.rubocop.yml
CHANGED
data/.rubocop_todo.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on 2026-05-
|
|
3
|
+
# on 2026-05-06 04:01:19 UTC using RuboCop version 1.86.1.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
@@ -11,16 +11,13 @@ Gemspec/RequiredRubyVersion:
|
|
|
11
11
|
Exclude:
|
|
12
12
|
- 'modspec.gemspec'
|
|
13
13
|
|
|
14
|
-
# Offense count:
|
|
14
|
+
# Offense count: 31
|
|
15
15
|
# This cop supports safe autocorrection (--autocorrect).
|
|
16
16
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
|
|
17
17
|
# URISchemes: http, https
|
|
18
18
|
Layout/LineLength:
|
|
19
19
|
Exclude:
|
|
20
|
-
- 'lib/modspec/conformance_class.rb'
|
|
21
20
|
- 'lib/modspec/conformance_test.rb'
|
|
22
|
-
- 'lib/modspec/normative_statement.rb'
|
|
23
|
-
- 'lib/modspec/normative_statements_class.rb'
|
|
24
21
|
- 'lib/modspec/suite.rb'
|
|
25
22
|
- 'spec/modspec/conformance_class_spec.rb'
|
|
26
23
|
- 'spec/modspec/conformance_test_spec.rb'
|
|
@@ -28,28 +25,14 @@ Layout/LineLength:
|
|
|
28
25
|
- 'spec/modspec/normative_statements_class_spec.rb'
|
|
29
26
|
- 'spec/modspec/suite_spec.rb'
|
|
30
27
|
|
|
31
|
-
# Offense count:
|
|
32
|
-
# Configuration parameters: AllowComments, AllowEmptyLambdas.
|
|
33
|
-
Lint/EmptyBlock:
|
|
34
|
-
Exclude:
|
|
35
|
-
- 'spec/modspec/conformance_class_spec.rb'
|
|
36
|
-
- 'spec/modspec/suite_spec.rb'
|
|
37
|
-
|
|
38
|
-
# Offense count: 1
|
|
39
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
40
|
-
# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions.
|
|
41
|
-
# NotImplementedExceptions: NotImplementedError
|
|
42
|
-
Lint/UnusedMethodArgument:
|
|
43
|
-
Exclude:
|
|
44
|
-
- 'lib/modspec/normative_statement.rb'
|
|
45
|
-
|
|
46
|
-
# Offense count: 7
|
|
28
|
+
# Offense count: 6
|
|
47
29
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
|
48
30
|
Metrics/AbcSize:
|
|
49
31
|
Exclude:
|
|
32
|
+
- 'lib/modspec/child_container.rb'
|
|
50
33
|
- 'lib/modspec/suite.rb'
|
|
51
34
|
|
|
52
|
-
# Offense count:
|
|
35
|
+
# Offense count: 4
|
|
53
36
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
54
37
|
Metrics/CyclomaticComplexity:
|
|
55
38
|
Exclude:
|
|
@@ -58,9 +41,9 @@ Metrics/CyclomaticComplexity:
|
|
|
58
41
|
# Offense count: 9
|
|
59
42
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
60
43
|
Metrics/MethodLength:
|
|
61
|
-
Max:
|
|
44
|
+
Max: 20
|
|
62
45
|
|
|
63
|
-
# Offense count:
|
|
46
|
+
# Offense count: 2
|
|
64
47
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
65
48
|
Metrics/PerceivedComplexity:
|
|
66
49
|
Exclude:
|
|
@@ -72,11 +55,6 @@ Performance/CollectionLiteralInLoop:
|
|
|
72
55
|
Exclude:
|
|
73
56
|
- 'lib/modspec/suite.rb'
|
|
74
57
|
|
|
75
|
-
# Offense count: 5
|
|
76
|
-
# Configuration parameters: CountAsOne.
|
|
77
|
-
RSpec/ExampleLength:
|
|
78
|
-
Max: 15
|
|
79
|
-
|
|
80
58
|
# Offense count: 1
|
|
81
59
|
# This cop supports safe autocorrection (--autocorrect).
|
|
82
60
|
RSpec/ExpectActual:
|
|
@@ -90,15 +68,10 @@ RSpec/IndexedLet:
|
|
|
90
68
|
- 'spec/modspec/normative_statements_class_spec.rb'
|
|
91
69
|
- 'spec/modspec/suite_spec.rb'
|
|
92
70
|
|
|
93
|
-
# Offense count:
|
|
71
|
+
# Offense count: 13
|
|
94
72
|
RSpec/MultipleExpectations:
|
|
95
73
|
Max: 9
|
|
96
74
|
|
|
97
|
-
# Offense count: 7
|
|
98
|
-
# Configuration parameters: AllowSubject.
|
|
99
|
-
RSpec/MultipleMemoizedHelpers:
|
|
100
|
-
Max: 12
|
|
101
|
-
|
|
102
75
|
# Offense count: 2
|
|
103
76
|
RSpec/RepeatedExample:
|
|
104
77
|
Exclude:
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Modspec
|
|
4
|
+
module ChildContainer
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend(ClassMethods)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def validates_children(collection_name, empty_label:, child_label:)
|
|
11
|
+
define_method(:validate_children_presence) do
|
|
12
|
+
children = send(collection_name)
|
|
13
|
+
if children.nil? || children.empty?
|
|
14
|
+
["#{empty_label} #{identifier} has no child #{child_label}"]
|
|
15
|
+
else
|
|
16
|
+
[]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
define_method(:validate_children_identifier_prefix) do
|
|
21
|
+
children = send(collection_name)
|
|
22
|
+
return [] unless children
|
|
23
|
+
|
|
24
|
+
expected_prefix = "#{identifier}/"
|
|
25
|
+
children.filter_map do |child|
|
|
26
|
+
unless child.identifier.to_s.start_with?(expected_prefix)
|
|
27
|
+
msg = "#{child_label} #{child.identifier} "
|
|
28
|
+
msg += "does not share the expected prefix #{expected_prefix}"
|
|
29
|
+
msg
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "lutaml/model"
|
|
4
|
-
require_relative "conformance_test"
|
|
5
|
-
require_relative "identifier"
|
|
6
4
|
|
|
7
5
|
module Modspec
|
|
8
6
|
class ConformanceClass < Lutaml::Model::Serializable
|
|
7
|
+
include ChildContainer
|
|
8
|
+
|
|
9
9
|
attribute :identifier, Identifier
|
|
10
10
|
attribute :name, :string
|
|
11
11
|
attribute :description, :string
|
|
@@ -17,6 +17,9 @@ module Modspec
|
|
|
17
17
|
attribute :belongs_to, Identifier, collection: true
|
|
18
18
|
attribute :reference, :string
|
|
19
19
|
|
|
20
|
+
validates_children :tests, empty_label: "Conformance class",
|
|
21
|
+
child_label: "conformance tests"
|
|
22
|
+
|
|
20
23
|
xml do
|
|
21
24
|
element "conformance-class"
|
|
22
25
|
map_attribute "identifier", to: :identifier
|
|
@@ -33,29 +36,10 @@ module Modspec
|
|
|
33
36
|
|
|
34
37
|
def validate
|
|
35
38
|
errors = super
|
|
36
|
-
errors.concat(
|
|
37
|
-
errors.concat(
|
|
39
|
+
errors.concat(validate_children_identifier_prefix)
|
|
40
|
+
errors.concat(validate_children_presence)
|
|
38
41
|
errors.concat(tests.flat_map(&:validate))
|
|
39
42
|
errors
|
|
40
43
|
end
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
def validate_class_children_mapping
|
|
45
|
-
if tests.nil? || tests.empty?
|
|
46
|
-
["Conformance class #{identifier} has no child conformance tests"]
|
|
47
|
-
else
|
|
48
|
-
[]
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def validate_identifier_prefix
|
|
53
|
-
errors = []
|
|
54
|
-
expected_prefix = "#{identifier}/"
|
|
55
|
-
tests&.each do |test|
|
|
56
|
-
errors << "Conformance test #{test.identifier} does not share the expected prefix #{expected_prefix}" unless test.identifier.to_s.start_with?(expected_prefix)
|
|
57
|
-
end
|
|
58
|
-
errors
|
|
59
|
-
end
|
|
60
44
|
end
|
|
61
45
|
end
|
data/lib/modspec/identifier.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "lutaml/model"
|
|
4
|
-
require_relative "identifier"
|
|
5
4
|
|
|
6
5
|
module Modspec
|
|
7
6
|
class NormativeStatementPart < Lutaml::Model::Serializable
|
|
@@ -46,43 +45,8 @@ module Modspec
|
|
|
46
45
|
map_element "parts", to: :parts
|
|
47
46
|
end
|
|
48
47
|
|
|
49
|
-
def validate(
|
|
50
|
-
|
|
51
|
-
errors.concat(validate_dependencies(suite)) if suite
|
|
52
|
-
errors.concat(validate_nested_requirement)
|
|
53
|
-
errors
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
def all_dependencies
|
|
59
|
-
(
|
|
60
|
-
(dependencies || []) +
|
|
61
|
-
(indirect_dependency || []) +
|
|
62
|
-
(implements || [])
|
|
63
|
-
).flatten.compact
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def validate_dependencies(suite)
|
|
67
|
-
errors = []
|
|
68
|
-
all_identifiers = suite.all_identifiers.map(&:to_s)
|
|
69
|
-
all_dependencies.each do |dep|
|
|
70
|
-
errors << "Requirement #{identifier} has an invalid dependency: #{dep}" unless all_identifiers.include?(dep)
|
|
71
|
-
end
|
|
72
|
-
errors
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def validate_nested_requirement
|
|
76
|
-
if has_parent_requirement?
|
|
77
|
-
["Nested requirement detected: #{identifier}"]
|
|
78
|
-
else
|
|
79
|
-
[]
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def has_parent_requirement?
|
|
84
|
-
# Implementation depends on how you determine if a requirement is nested
|
|
85
|
-
false
|
|
48
|
+
def validate(register: Lutaml::Model::Config.default_register)
|
|
49
|
+
super
|
|
86
50
|
end
|
|
87
51
|
end
|
|
88
52
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "lutaml/model"
|
|
4
|
-
require_relative "normative_statement"
|
|
5
|
-
require_relative "identifier"
|
|
6
4
|
|
|
7
5
|
module Modspec
|
|
8
6
|
class NormativeStatementsClass < Lutaml::Model::Serializable
|
|
7
|
+
include ChildContainer
|
|
8
|
+
|
|
9
9
|
attribute :identifier, Identifier
|
|
10
10
|
attribute :name, :string
|
|
11
11
|
attribute :description, :string
|
|
@@ -18,6 +18,9 @@ module Modspec
|
|
|
18
18
|
attribute :reference, :string
|
|
19
19
|
attribute :source, :string
|
|
20
20
|
|
|
21
|
+
validates_children :normative_statements, empty_label: "Requirement class",
|
|
22
|
+
child_label: "requirements"
|
|
23
|
+
|
|
21
24
|
xml do
|
|
22
25
|
element "normative-statements-class"
|
|
23
26
|
map_attribute "identifier", to: :identifier
|
|
@@ -33,31 +36,12 @@ module Modspec
|
|
|
33
36
|
map_element "source", to: :source
|
|
34
37
|
end
|
|
35
38
|
|
|
36
|
-
def validate
|
|
37
|
-
errors = super
|
|
38
|
-
errors.concat(
|
|
39
|
-
errors.concat(
|
|
40
|
-
errors.concat(normative_statements.flat_map
|
|
39
|
+
def validate
|
|
40
|
+
errors = super
|
|
41
|
+
errors.concat(validate_children_identifier_prefix)
|
|
42
|
+
errors.concat(validate_children_presence)
|
|
43
|
+
errors.concat(normative_statements.flat_map(&:validate))
|
|
41
44
|
errors
|
|
42
45
|
end
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
def validate_identifier_prefix
|
|
47
|
-
return [] if normative_statements.nil? || normative_statements.empty?
|
|
48
|
-
|
|
49
|
-
expected_prefix = "#{identifier}/"
|
|
50
|
-
normative_statements.each_with_object([]) do |statement, errors|
|
|
51
|
-
errors << "Normative statement #{statement.identifier} does not share the expected prefix #{expected_prefix}" unless statement.identifier.to_s.start_with?(expected_prefix)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def validate_class_children_mapping
|
|
56
|
-
if normative_statements.nil? || normative_statements.empty?
|
|
57
|
-
["Requirement class #{identifier} has no child requirements"]
|
|
58
|
-
else
|
|
59
|
-
[]
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
46
|
end
|
|
63
47
|
end
|
data/lib/modspec/suite.rb
CHANGED
|
@@ -21,15 +21,13 @@ module Modspec
|
|
|
21
21
|
|
|
22
22
|
def validate
|
|
23
23
|
setup_relationships
|
|
24
|
-
|
|
24
|
+
reset_statement_index
|
|
25
25
|
errors = super
|
|
26
26
|
errors.concat(validate_cycles)
|
|
27
27
|
errors.concat(validate_label_uniqueness)
|
|
28
28
|
errors.concat(validate_dependencies)
|
|
29
29
|
unless normative_statements_classes.nil?
|
|
30
|
-
errors.concat(normative_statements_classes.flat_map
|
|
31
|
-
n.validate(self)
|
|
32
|
-
end)
|
|
30
|
+
errors.concat(normative_statements_classes.flat_map(&:validate))
|
|
33
31
|
end
|
|
34
32
|
errors.concat(conformance_classes.flat_map(&:validate)) unless conformance_classes.nil?
|
|
35
33
|
errors
|
|
@@ -42,7 +40,7 @@ module Modspec
|
|
|
42
40
|
end
|
|
43
41
|
|
|
44
42
|
combined_suite = dup
|
|
45
|
-
combined_suite.
|
|
43
|
+
combined_suite.reset_statement_index
|
|
46
44
|
if other_suite.normative_statements_classes
|
|
47
45
|
combined_suite.normative_statements_classes ||= []
|
|
48
46
|
combined_suite.normative_statements_classes += other_suite.normative_statements_classes
|
|
@@ -63,19 +61,17 @@ module Modspec
|
|
|
63
61
|
combined_suite
|
|
64
62
|
end
|
|
65
63
|
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
nsc = normative_statements_classes || []
|
|
70
|
-
cc = conformance_classes || []
|
|
64
|
+
def statement_index
|
|
65
|
+
@statement_index ||= build_statement_index
|
|
66
|
+
end
|
|
71
67
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
nsc +
|
|
75
|
-
cc).map(&:identifier)
|
|
68
|
+
def reset_statement_index
|
|
69
|
+
@statement_index = nil
|
|
76
70
|
end
|
|
77
71
|
|
|
78
|
-
|
|
72
|
+
def all_identifiers
|
|
73
|
+
statement_index.keys
|
|
74
|
+
end
|
|
79
75
|
|
|
80
76
|
def resolve_conflicts(other_suite)
|
|
81
77
|
resolve_conflicts_for(normative_statements_classes,
|
|
@@ -95,18 +91,17 @@ module Modspec
|
|
|
95
91
|
end
|
|
96
92
|
|
|
97
93
|
def setup_relationships
|
|
98
|
-
|
|
99
|
-
normative_statements_classes.flat_map(&:normative_statements)
|
|
100
|
-
else
|
|
101
|
-
[]
|
|
102
|
-
end
|
|
94
|
+
return unless normative_statements_classes && conformance_classes
|
|
103
95
|
|
|
104
|
-
|
|
96
|
+
req_index = normative_statements_classes
|
|
97
|
+
.flat_map(&:normative_statements)
|
|
98
|
+
.to_h { |r| [r.identifier.to_s, r] }
|
|
105
99
|
|
|
106
100
|
conformance_classes.each do |cc|
|
|
107
101
|
cc.tests.each do |ct|
|
|
108
|
-
|
|
109
|
-
|
|
102
|
+
targets = Array(ct.targets).map(&:to_s)
|
|
103
|
+
ct.corresponding_requirements = targets.filter_map do |t|
|
|
104
|
+
req_index[t]
|
|
110
105
|
end
|
|
111
106
|
ct.parent_class = cc
|
|
112
107
|
end
|
|
@@ -132,13 +127,16 @@ module Modspec
|
|
|
132
127
|
end
|
|
133
128
|
|
|
134
129
|
def merge_attributes(existing_item, other_item)
|
|
135
|
-
existing_item.class.
|
|
130
|
+
existing_item.class.attributes.each_key do |attr|
|
|
136
131
|
next if %i[identifier name].include?(attr)
|
|
137
132
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
133
|
+
existing_val = existing_item.send(attr)
|
|
134
|
+
other_val = other_item.send(attr)
|
|
135
|
+
|
|
136
|
+
if existing_val.is_a?(Array)
|
|
137
|
+
existing_item.send(attr).concat(other_val).uniq!
|
|
138
|
+
elsif existing_val.nil? && !other_val.nil?
|
|
139
|
+
existing_item.send("#{attr}=", other_val)
|
|
142
140
|
end
|
|
143
141
|
end
|
|
144
142
|
end
|
|
@@ -149,21 +147,20 @@ module Modspec
|
|
|
149
147
|
cycles.map { |cycle| "Cycle detected: #{cycle.join(' -> ')}" }
|
|
150
148
|
end
|
|
151
149
|
|
|
152
|
-
# Combine all statements into a single array
|
|
153
|
-
# This includes both normative statements and conformance tests
|
|
154
150
|
def all_statements
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
151
|
+
statement_index.values
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def each_statement(&block)
|
|
155
|
+
normative_statements_classes&.each do |nsc|
|
|
156
|
+
yield nsc
|
|
157
|
+
nsc.normative_statements.each(&block)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
conformance_classes&.each do |cc|
|
|
161
|
+
yield cc
|
|
162
|
+
cc.tests.each(&block)
|
|
163
|
+
end
|
|
167
164
|
end
|
|
168
165
|
|
|
169
166
|
def build_dependency_graph
|
|
@@ -171,15 +168,14 @@ module Modspec
|
|
|
171
168
|
|
|
172
169
|
all_statements.each do |statement|
|
|
173
170
|
id = statement.identifier.to_s
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
# Define all dependency-like properties to check
|
|
177
|
-
dependency_properties = %i[dependencies indirect_dependency implements
|
|
178
|
-
targets]
|
|
171
|
+
deps = Set.new
|
|
179
172
|
|
|
180
|
-
|
|
181
|
-
|
|
173
|
+
%i[dependencies indirect_dependency implements targets].each do |prop|
|
|
174
|
+
refs = statement.send(prop) if statement.respond_to?(prop)
|
|
175
|
+
deps.merge(refs.map(&:to_s)) if refs
|
|
182
176
|
end
|
|
177
|
+
|
|
178
|
+
graph[id] = deps
|
|
183
179
|
end
|
|
184
180
|
|
|
185
181
|
graph
|
|
@@ -205,20 +201,14 @@ module Modspec
|
|
|
205
201
|
recursion_stack.add(node)
|
|
206
202
|
path.push(node)
|
|
207
203
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
elsif recursion_stack.include?(neighbor)
|
|
216
|
-
return path[path.index(neighbor)..] + [neighbor]
|
|
217
|
-
end
|
|
204
|
+
graph[node]&.each do |neighbor|
|
|
205
|
+
if !visited.include?(neighbor)
|
|
206
|
+
cycle = detect_cycle_util(neighbor, graph, visited,
|
|
207
|
+
recursion_stack, path)
|
|
208
|
+
return cycle if cycle
|
|
209
|
+
elsif recursion_stack.include?(neighbor)
|
|
210
|
+
return path[path.index(neighbor)..] + [neighbor]
|
|
218
211
|
end
|
|
219
|
-
else
|
|
220
|
-
# If the node doesn't exist in the graph, log a warning
|
|
221
|
-
puts "Warning: Node #{node} referenced but not found in the graph"
|
|
222
212
|
end
|
|
223
213
|
|
|
224
214
|
path.pop
|
|
@@ -227,81 +217,60 @@ module Modspec
|
|
|
227
217
|
end
|
|
228
218
|
|
|
229
219
|
def validate_label_uniqueness
|
|
230
|
-
|
|
220
|
+
seen = {}
|
|
231
221
|
errors = []
|
|
232
|
-
|
|
233
|
-
|
|
222
|
+
each_statement do |statement|
|
|
223
|
+
id = statement.identifier.to_s
|
|
224
|
+
if seen[id]
|
|
234
225
|
errors << "Duplicate identifier found: #{statement.identifier}"
|
|
235
226
|
else
|
|
236
|
-
|
|
227
|
+
seen[id] = true
|
|
237
228
|
end
|
|
238
229
|
end
|
|
239
230
|
errors
|
|
240
231
|
end
|
|
241
232
|
|
|
242
233
|
def validate_dependencies
|
|
243
|
-
|
|
234
|
+
all_ids = statement_index
|
|
244
235
|
|
|
245
236
|
errors = []
|
|
246
237
|
normative_statements_classes&.each do |nsc|
|
|
247
|
-
errors.concat(
|
|
238
|
+
errors.concat(validate_refs(nsc, all_ids, :dependencies))
|
|
239
|
+
errors.concat(validate_refs(nsc, all_ids, :implements))
|
|
248
240
|
nsc.normative_statements.each do |ns|
|
|
249
|
-
errors.concat(
|
|
241
|
+
errors.concat(validate_refs(ns, all_ids, :dependencies))
|
|
242
|
+
errors.concat(validate_refs(ns, all_ids, :indirect_dependency))
|
|
243
|
+
errors.concat(validate_refs(ns, all_ids, :implements))
|
|
250
244
|
end
|
|
251
245
|
end
|
|
252
246
|
|
|
253
247
|
conformance_classes&.each do |cc|
|
|
254
|
-
errors.concat(
|
|
248
|
+
errors.concat(validate_refs(cc, all_ids, :dependencies))
|
|
255
249
|
cc.tests.each do |ct|
|
|
256
|
-
errors.concat(
|
|
250
|
+
errors.concat(validate_refs(ct, all_ids, :dependencies))
|
|
251
|
+
errors.concat(validate_refs(ct, all_ids, :targets))
|
|
257
252
|
end
|
|
258
253
|
end
|
|
259
254
|
|
|
260
255
|
errors
|
|
261
256
|
end
|
|
262
257
|
|
|
263
|
-
def
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
identifiers[nsc.identifier.to_s] = nsc
|
|
268
|
-
nsc.normative_statements.each do |ns|
|
|
269
|
-
identifiers[ns.identifier.to_s] = ns
|
|
270
|
-
end
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
conformance_classes&.each do |cc|
|
|
274
|
-
identifiers[cc.identifier.to_s] = cc
|
|
275
|
-
cc.tests.each do |ct|
|
|
276
|
-
identifiers[ct.identifier.to_s] = ct
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
identifiers
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
def validate_class_dependencies(klass, all_identifiers)
|
|
284
|
-
errors = []
|
|
285
|
-
klass.dependencies&.each do |dep|
|
|
286
|
-
errors << "Invalid dependency #{dep} in #{klass.identifier}" unless all_identifiers.key?(dep.to_s)
|
|
287
|
-
end
|
|
288
|
-
errors
|
|
258
|
+
def build_statement_index
|
|
259
|
+
index = {}
|
|
260
|
+
each_statement { |s| index[s.identifier.to_s] = s }
|
|
261
|
+
index
|
|
289
262
|
end
|
|
290
263
|
|
|
291
|
-
def
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
errors << "Invalid dependency #{dep} in #{statement.identifier}" unless all_identifiers.key?(dep.to_s)
|
|
295
|
-
end
|
|
296
|
-
errors
|
|
297
|
-
end
|
|
264
|
+
def validate_refs(obj, all_ids, property)
|
|
265
|
+
refs = obj.send(property)
|
|
266
|
+
return [] unless refs
|
|
298
267
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
268
|
+
refs.filter_map do |ref|
|
|
269
|
+
unless all_ids.key?(ref.to_s)
|
|
270
|
+
"Invalid #{property.to_s.tr('_',
|
|
271
|
+
' ')} #{ref} in #{obj.identifier}"
|
|
272
|
+
end
|
|
303
273
|
end
|
|
304
|
-
errors
|
|
305
274
|
end
|
|
306
275
|
end
|
|
307
276
|
end
|
data/lib/modspec/version.rb
CHANGED
data/lib/modspec.rb
CHANGED
|
@@ -5,14 +5,12 @@ require "lutaml/model"
|
|
|
5
5
|
require "set"
|
|
6
6
|
|
|
7
7
|
module Modspec
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
autoload :ChildContainer, "modspec/child_container"
|
|
9
|
+
autoload :Identifier, "modspec/identifier"
|
|
10
|
+
autoload :NormativeStatement, "modspec/normative_statement"
|
|
11
|
+
autoload :NormativeStatementPart, "modspec/normative_statement"
|
|
12
|
+
autoload :NormativeStatementsClass, "modspec/normative_statements_class"
|
|
13
|
+
autoload :ConformanceTest, "modspec/conformance_test"
|
|
14
|
+
autoload :ConformanceClass, "modspec/conformance_class"
|
|
15
|
+
autoload :Suite, "modspec/suite"
|
|
11
16
|
end
|
|
12
|
-
|
|
13
|
-
require_relative "modspec/identifier"
|
|
14
|
-
require_relative "modspec/normative_statement"
|
|
15
|
-
require_relative "modspec/normative_statements_class"
|
|
16
|
-
require_relative "modspec/conformance_test"
|
|
17
|
-
require_relative "modspec/conformance_class"
|
|
18
|
-
require_relative "modspec/suite"
|
|
@@ -11,12 +11,12 @@ RSpec.describe Modspec::ConformanceClass do
|
|
|
11
11
|
Modspec::NormativeStatement.new(
|
|
12
12
|
identifier: "/req/basic-ypr/position",
|
|
13
13
|
name: "Expression of outer frame",
|
|
14
|
-
statement: "The `Basic_YPR.position` attribute shall represent the outer frame
|
|
14
|
+
statement: "The `Basic_YPR.position` attribute shall represent the outer frame.",
|
|
15
15
|
),
|
|
16
16
|
Modspec::NormativeStatement.new(
|
|
17
17
|
identifier: "/req/basic-ypr/angles",
|
|
18
18
|
name: "Expression of inner frame",
|
|
19
|
-
statement: "The `Basic_YPR.angles` attribute shall represent the inner frame
|
|
19
|
+
statement: "The `Basic_YPR.angles` attribute shall represent the inner frame.",
|
|
20
20
|
),
|
|
21
21
|
],
|
|
22
22
|
)
|
|
@@ -35,7 +35,7 @@ RSpec.describe Modspec::ConformanceClass do
|
|
|
35
35
|
identifier: "/conf/basic-ypr/position",
|
|
36
36
|
name: "Verify expression of outer frame",
|
|
37
37
|
targets: ["/req/basic-ypr/position"],
|
|
38
|
-
description: "To confirm
|
|
38
|
+
description: "To confirm outer frame.",
|
|
39
39
|
purpose: "Verify that this requirement is satisfied.",
|
|
40
40
|
test_method: "Inspection",
|
|
41
41
|
),
|
|
@@ -43,7 +43,7 @@ RSpec.describe Modspec::ConformanceClass do
|
|
|
43
43
|
identifier: "/conf/basic-ypr/angles",
|
|
44
44
|
name: "Verify expression of inner frame",
|
|
45
45
|
targets: ["/req/basic-ypr/angles"],
|
|
46
|
-
description: "To confirm
|
|
46
|
+
description: "To confirm inner frame.",
|
|
47
47
|
purpose: "Verify that this requirement is satisfied.",
|
|
48
48
|
test_method: "Inspection",
|
|
49
49
|
),
|
|
@@ -88,7 +88,7 @@ RSpec.describe Modspec::ConformanceClass do
|
|
|
88
88
|
identifier: "/conf/global/sdu",
|
|
89
89
|
name: "Verify SDU conformance",
|
|
90
90
|
targets: ["/req/global/sdu"],
|
|
91
|
-
description: "To confirm
|
|
91
|
+
description: "To confirm SDU conformance.",
|
|
92
92
|
purpose: "Verify that this requirement is satisfied.",
|
|
93
93
|
test_method: "Inspection",
|
|
94
94
|
),
|
|
@@ -105,11 +105,10 @@ RSpec.describe Modspec::ConformanceClass do
|
|
|
105
105
|
identifier: "/conf/tangent-point/height",
|
|
106
106
|
name: "Verify tangent point height",
|
|
107
107
|
targets: ["/req/tangent-point/height"],
|
|
108
|
-
description: "To confirm
|
|
108
|
+
description: "To confirm tangent point height.",
|
|
109
109
|
purpose: "Verify that this requirement is satisfied.",
|
|
110
110
|
test_method: "Inspection",
|
|
111
111
|
),
|
|
112
|
-
|
|
113
112
|
],
|
|
114
113
|
)
|
|
115
114
|
end
|
|
@@ -140,17 +139,25 @@ RSpec.describe Modspec::ConformanceClass do
|
|
|
140
139
|
describe "#validate" do
|
|
141
140
|
it "returns no errors for a valid conformance class" do
|
|
142
141
|
errors = suite.validate
|
|
143
|
-
if errors.any?
|
|
144
|
-
|
|
145
|
-
errors.each { |error| }
|
|
146
|
-
end
|
|
147
142
|
expect(errors).to be_empty
|
|
148
143
|
end
|
|
149
144
|
|
|
150
145
|
it "returns errors if there are no conformance tests" do
|
|
151
146
|
conformance_class.tests = []
|
|
152
147
|
errors = conformance_class.validate
|
|
153
|
-
expect(errors).
|
|
148
|
+
expect(errors).to include(a_string_matching(/no child conformance tests/))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "returns errors if test identifier does not share prefix" do
|
|
152
|
+
conformance_class.tests = [
|
|
153
|
+
Modspec::ConformanceTest.new(
|
|
154
|
+
identifier: "/conf/other/test",
|
|
155
|
+
name: "Mismatched test",
|
|
156
|
+
targets: ["/req/basic-ypr/position"],
|
|
157
|
+
),
|
|
158
|
+
]
|
|
159
|
+
errors = conformance_class.validate
|
|
160
|
+
expect(errors).to include(a_string_matching(/does not share the expected prefix/))
|
|
154
161
|
end
|
|
155
162
|
end
|
|
156
163
|
end
|
|
@@ -5,7 +5,7 @@ RSpec.describe Modspec::ConformanceTest do
|
|
|
5
5
|
Modspec::NormativeStatement.new(
|
|
6
6
|
identifier: "/req/basic-ypr/position",
|
|
7
7
|
name: "Expression of outer frame",
|
|
8
|
-
statement: "The `Basic_YPR.position` attribute shall represent the outer frame
|
|
8
|
+
statement: "The `Basic_YPR.position` attribute shall represent the outer frame.",
|
|
9
9
|
)
|
|
10
10
|
end
|
|
11
11
|
|
|
@@ -22,7 +22,7 @@ RSpec.describe Modspec::ConformanceTest do
|
|
|
22
22
|
identifier: "/conf/basic-ypr/position",
|
|
23
23
|
name: "Verify expression of outer frame",
|
|
24
24
|
targets: ["/req/basic-ypr/position"],
|
|
25
|
-
description: "To confirm
|
|
25
|
+
description: "To confirm outer frame.",
|
|
26
26
|
purpose: "Verify that this requirement is satisfied.",
|
|
27
27
|
test_method: "Inspection",
|
|
28
28
|
)
|
|
@@ -45,7 +45,7 @@ RSpec.describe Modspec::ConformanceTest do
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
before do
|
|
48
|
-
suite
|
|
48
|
+
suite
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
it "has an identifier" do
|
|
@@ -65,6 +65,24 @@ RSpec.describe Modspec::ConformanceTest do
|
|
|
65
65
|
errors = conformance_test.validate
|
|
66
66
|
expect(errors).to be_empty
|
|
67
67
|
end
|
|
68
|
+
|
|
69
|
+
it "returns errors when corresponding_requirements is nil" do
|
|
70
|
+
conformance_test.corresponding_requirements = nil
|
|
71
|
+
errors = conformance_test.validate
|
|
72
|
+
expect(errors).to include(a_string_matching(/no corresponding requirements/))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "returns errors when corresponding_requirements is empty" do
|
|
76
|
+
conformance_test.corresponding_requirements = []
|
|
77
|
+
errors = conformance_test.validate
|
|
78
|
+
expect(errors).to include(a_string_matching(/no corresponding requirements/))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "returns errors when parent_class is nil" do
|
|
82
|
+
conformance_test.parent_class = nil
|
|
83
|
+
errors = conformance_test.validate
|
|
84
|
+
expect(errors).to include(a_string_matching(/does not belong to its parent class/))
|
|
85
|
+
end
|
|
68
86
|
end
|
|
69
87
|
|
|
70
88
|
it "has a corresponding requirement" do
|
|
@@ -5,7 +5,7 @@ RSpec.describe Modspec::NormativeStatementsClass do
|
|
|
5
5
|
Modspec::NormativeStatement.new(
|
|
6
6
|
identifier: "/req/basic-ypr/position",
|
|
7
7
|
name: "Expression of outer frame",
|
|
8
|
-
statement: "The `Basic_YPR.position` attribute shall represent the outer frame
|
|
8
|
+
statement: "The `Basic_YPR.position` attribute shall represent the outer frame.",
|
|
9
9
|
)
|
|
10
10
|
end
|
|
11
11
|
|
|
@@ -13,7 +13,7 @@ RSpec.describe Modspec::NormativeStatementsClass do
|
|
|
13
13
|
Modspec::NormativeStatement.new(
|
|
14
14
|
identifier: "/req/basic-ypr/angles",
|
|
15
15
|
name: "Expression of inner frame",
|
|
16
|
-
statement: "The `Basic_YPR.angles` attribute shall represent the inner frame
|
|
16
|
+
statement: "The `Basic_YPR.angles` attribute shall represent the inner frame.",
|
|
17
17
|
)
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -21,7 +21,7 @@ RSpec.describe Modspec::NormativeStatementsClass do
|
|
|
21
21
|
described_class.new(
|
|
22
22
|
identifier: "/req/basic-ypr",
|
|
23
23
|
name: "Basic-YPR logical model SDU",
|
|
24
|
-
description: "The Basic-YPR Target has a simple structure
|
|
24
|
+
description: "The Basic-YPR Target has a simple structure.",
|
|
25
25
|
dependencies: ["/req/global", "/req/tangent-point"],
|
|
26
26
|
normative_statements: [normative_statement1, normative_statement2],
|
|
27
27
|
)
|
|
@@ -55,7 +55,19 @@ RSpec.describe Modspec::NormativeStatementsClass do
|
|
|
55
55
|
it "returns errors if there are no normative statements" do
|
|
56
56
|
normative_statements_class.normative_statements = []
|
|
57
57
|
errors = normative_statements_class.validate
|
|
58
|
-
expect(errors).
|
|
58
|
+
expect(errors).to include(a_string_matching(/no child requirements/))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "returns errors if statement identifier does not share prefix" do
|
|
62
|
+
normative_statements_class.normative_statements = [
|
|
63
|
+
Modspec::NormativeStatement.new(
|
|
64
|
+
identifier: "/req/other/mismatch",
|
|
65
|
+
name: "Mismatched",
|
|
66
|
+
statement: "stmt",
|
|
67
|
+
),
|
|
68
|
+
]
|
|
69
|
+
errors = normative_statements_class.validate
|
|
70
|
+
expect(errors).to include(a_string_matching(/does not share the expected prefix/))
|
|
59
71
|
end
|
|
60
72
|
end
|
|
61
73
|
end
|
data/spec/modspec/suite_spec.rb
CHANGED
|
@@ -38,12 +38,204 @@ RSpec.describe Modspec::Suite do
|
|
|
38
38
|
.combine(frame_spec_suite)
|
|
39
39
|
|
|
40
40
|
errors = combined_suite.validate
|
|
41
|
-
if errors.any?
|
|
42
|
-
|
|
43
|
-
errors.each { |error| }
|
|
44
|
-
end
|
|
45
41
|
expect(errors).to be_empty
|
|
46
42
|
end
|
|
43
|
+
|
|
44
|
+
it "detects duplicate identifiers" do
|
|
45
|
+
suite = described_class.new(
|
|
46
|
+
identifier: "/suite",
|
|
47
|
+
name: "Test",
|
|
48
|
+
normative_statements_classes: [
|
|
49
|
+
Modspec::NormativeStatementsClass.new(
|
|
50
|
+
identifier: "/req/test",
|
|
51
|
+
normative_statements: [
|
|
52
|
+
Modspec::NormativeStatement.new(identifier: "/req/test/a",
|
|
53
|
+
name: "A", statement: "a"),
|
|
54
|
+
Modspec::NormativeStatement.new(identifier: "/req/test/a",
|
|
55
|
+
name: "A2", statement: "a2"),
|
|
56
|
+
],
|
|
57
|
+
),
|
|
58
|
+
],
|
|
59
|
+
conformance_classes: [],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
errors = suite.validate
|
|
63
|
+
expect(errors).to include(a_string_matching(/Duplicate identifier/))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "detects cross-type identifier collisions" do
|
|
67
|
+
suite = described_class.new(
|
|
68
|
+
identifier: "/suite",
|
|
69
|
+
name: "Test",
|
|
70
|
+
normative_statements_classes: [
|
|
71
|
+
Modspec::NormativeStatementsClass.new(
|
|
72
|
+
identifier: "/req/test",
|
|
73
|
+
normative_statements: [
|
|
74
|
+
Modspec::NormativeStatement.new(identifier: "/req/test/a",
|
|
75
|
+
name: "A", statement: "a"),
|
|
76
|
+
],
|
|
77
|
+
),
|
|
78
|
+
],
|
|
79
|
+
conformance_classes: [
|
|
80
|
+
Modspec::ConformanceClass.new(
|
|
81
|
+
identifier: "/conf/test",
|
|
82
|
+
tests: [
|
|
83
|
+
Modspec::ConformanceTest.new(
|
|
84
|
+
identifier: "/req/test/a",
|
|
85
|
+
name: "CT collision",
|
|
86
|
+
),
|
|
87
|
+
],
|
|
88
|
+
),
|
|
89
|
+
],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
errors = suite.validate
|
|
93
|
+
expect(errors).to include(a_string_matching(/Duplicate identifier.*\/req\/test\/a/))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "detects dependency cycles" do
|
|
97
|
+
ns_a = Modspec::NormativeStatement.new(
|
|
98
|
+
identifier: "/req/test/a", name: "A", statement: "a",
|
|
99
|
+
dependencies: ["/req/test/b"]
|
|
100
|
+
)
|
|
101
|
+
ns_b = Modspec::NormativeStatement.new(
|
|
102
|
+
identifier: "/req/test/b", name: "B", statement: "b",
|
|
103
|
+
dependencies: ["/req/test/a"]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
suite = described_class.new(
|
|
107
|
+
identifier: "/suite",
|
|
108
|
+
name: "Test",
|
|
109
|
+
normative_statements_classes: [
|
|
110
|
+
Modspec::NormativeStatementsClass.new(
|
|
111
|
+
identifier: "/req/test",
|
|
112
|
+
normative_statements: [ns_a, ns_b],
|
|
113
|
+
),
|
|
114
|
+
],
|
|
115
|
+
conformance_classes: [],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
errors = suite.validate
|
|
119
|
+
expect(errors).to include(a_string_matching(/Cycle detected/))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "detects invalid dependencies" do
|
|
123
|
+
suite = described_class.new(
|
|
124
|
+
identifier: "/suite",
|
|
125
|
+
name: "Test",
|
|
126
|
+
normative_statements_classes: [
|
|
127
|
+
Modspec::NormativeStatementsClass.new(
|
|
128
|
+
identifier: "/req/test",
|
|
129
|
+
dependencies: ["/req/nonexistent"],
|
|
130
|
+
normative_statements: [
|
|
131
|
+
Modspec::NormativeStatement.new(
|
|
132
|
+
identifier: "/req/test/a", name: "A", statement: "a",
|
|
133
|
+
dependencies: ["/req/missing"]
|
|
134
|
+
),
|
|
135
|
+
],
|
|
136
|
+
),
|
|
137
|
+
],
|
|
138
|
+
conformance_classes: [],
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
errors = suite.validate
|
|
142
|
+
expect(errors).to include(a_string_matching(/Invalid dependencies .* in \/req\/test\b/))
|
|
143
|
+
expect(errors).to include(a_string_matching(/Invalid dependencies .* in \/req\/test\/a/))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it "detects invalid conformance test targets" do
|
|
147
|
+
suite = described_class.new(
|
|
148
|
+
identifier: "/suite",
|
|
149
|
+
name: "Test",
|
|
150
|
+
normative_statements_classes: [
|
|
151
|
+
Modspec::NormativeStatementsClass.new(
|
|
152
|
+
identifier: "/req/test",
|
|
153
|
+
normative_statements: [
|
|
154
|
+
Modspec::NormativeStatement.new(identifier: "/req/test/a",
|
|
155
|
+
name: "A", statement: "a"),
|
|
156
|
+
],
|
|
157
|
+
),
|
|
158
|
+
],
|
|
159
|
+
conformance_classes: [
|
|
160
|
+
Modspec::ConformanceClass.new(
|
|
161
|
+
identifier: "/conf/test",
|
|
162
|
+
tests: [
|
|
163
|
+
Modspec::ConformanceTest.new(
|
|
164
|
+
identifier: "/conf/test/a",
|
|
165
|
+
name: "CT-A",
|
|
166
|
+
targets: ["/req/nonexistent"],
|
|
167
|
+
),
|
|
168
|
+
],
|
|
169
|
+
),
|
|
170
|
+
],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
errors = suite.validate
|
|
174
|
+
expect(errors).to include(a_string_matching(/Invalid targets .* in \/conf\/test\/a/))
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "detects invalid indirect_dependency references" do
|
|
178
|
+
suite = described_class.new(
|
|
179
|
+
identifier: "/suite",
|
|
180
|
+
name: "Test",
|
|
181
|
+
normative_statements_classes: [
|
|
182
|
+
Modspec::NormativeStatementsClass.new(
|
|
183
|
+
identifier: "/req/test",
|
|
184
|
+
normative_statements: [
|
|
185
|
+
Modspec::NormativeStatement.new(
|
|
186
|
+
identifier: "/req/test/a", name: "A", statement: "a",
|
|
187
|
+
indirect_dependency: ["/req/ghost"]
|
|
188
|
+
),
|
|
189
|
+
],
|
|
190
|
+
),
|
|
191
|
+
],
|
|
192
|
+
conformance_classes: [],
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
errors = suite.validate
|
|
196
|
+
expect(errors).to include(a_string_matching(/indirect dependency.*in \/req\/test\/a/))
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it "detects invalid implements references" do
|
|
200
|
+
suite = described_class.new(
|
|
201
|
+
identifier: "/suite",
|
|
202
|
+
name: "Test",
|
|
203
|
+
normative_statements_classes: [
|
|
204
|
+
Modspec::NormativeStatementsClass.new(
|
|
205
|
+
identifier: "/req/test",
|
|
206
|
+
implements: ["/req/phantom"],
|
|
207
|
+
normative_statements: [],
|
|
208
|
+
),
|
|
209
|
+
],
|
|
210
|
+
conformance_classes: [],
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
errors = suite.validate
|
|
214
|
+
expect(errors).to include(a_string_matching(/implements.*in \/req\/test/))
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it "detects invalid conformance test dependencies" do
|
|
218
|
+
suite = described_class.new(
|
|
219
|
+
identifier: "/suite",
|
|
220
|
+
name: "Test",
|
|
221
|
+
normative_statements_classes: [],
|
|
222
|
+
conformance_classes: [
|
|
223
|
+
Modspec::ConformanceClass.new(
|
|
224
|
+
identifier: "/conf/test",
|
|
225
|
+
tests: [
|
|
226
|
+
Modspec::ConformanceTest.new(
|
|
227
|
+
identifier: "/conf/test/a",
|
|
228
|
+
name: "CT-A",
|
|
229
|
+
dependencies: ["/conf/nonexistent"],
|
|
230
|
+
),
|
|
231
|
+
],
|
|
232
|
+
),
|
|
233
|
+
],
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
errors = suite.validate
|
|
237
|
+
expect(errors).to include(a_string_matching(/Invalid dependencies .* in \/conf\/test\/a/))
|
|
238
|
+
end
|
|
47
239
|
end
|
|
48
240
|
|
|
49
241
|
describe "#combine" do
|
|
@@ -63,6 +255,16 @@ RSpec.describe Modspec::Suite do
|
|
|
63
255
|
)
|
|
64
256
|
end
|
|
65
257
|
|
|
258
|
+
it "raises ArgumentError for non-Suite argument" do
|
|
259
|
+
expect { suite1.combine("not a suite") }.to raise_error(ArgumentError)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it "deduplicates by identifier" do
|
|
263
|
+
combined = suite1.combine(suite1)
|
|
264
|
+
nsc_count = combined.normative_statements_classes.count
|
|
265
|
+
expect(nsc_count).to eq(suite1.normative_statements_classes.count)
|
|
266
|
+
end
|
|
267
|
+
|
|
66
268
|
it "resolves conflicts when combining suites" do
|
|
67
269
|
combined_suite = suite1
|
|
68
270
|
.combine(suite2)
|
|
@@ -72,10 +274,6 @@ RSpec.describe Modspec::Suite do
|
|
|
72
274
|
.combine(frame_spec_suite)
|
|
73
275
|
|
|
74
276
|
errors = combined_suite.validate
|
|
75
|
-
if errors.any?
|
|
76
|
-
|
|
77
|
-
errors.each { |error| }
|
|
78
|
-
end
|
|
79
277
|
expect(errors).to be_empty
|
|
80
278
|
end
|
|
81
279
|
end
|
|
@@ -89,25 +287,212 @@ RSpec.describe Modspec::Suite do
|
|
|
89
287
|
expect(combined_suite).to be_a(described_class)
|
|
90
288
|
expect(combined_suite.name).to eq("Combined Suite")
|
|
91
289
|
|
|
92
|
-
# Ensure the combined suite has content
|
|
93
290
|
expect(combined_suite.normative_statements_classes).not_to be_empty
|
|
94
291
|
expect(combined_suite.conformance_classes).not_to be_empty
|
|
95
292
|
|
|
96
|
-
# Validate the combined suite
|
|
97
293
|
errors = combined_suite.validate
|
|
294
|
+
expect(errors).not_to include(a_string_matching(/has no corresponding requirement/))
|
|
295
|
+
expect(errors).not_to include(a_string_matching(/Cycle detected/))
|
|
296
|
+
expect(errors).not_to include(a_string_matching(/has an invalid dependency/))
|
|
297
|
+
expect(errors).to be_empty
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
describe "#setup_relationships" do
|
|
302
|
+
it "links conformance tests to their target normative statements" do
|
|
303
|
+
suite = described_class.new(
|
|
304
|
+
identifier: "/suite",
|
|
305
|
+
name: "Test",
|
|
306
|
+
normative_statements_classes: [
|
|
307
|
+
Modspec::NormativeStatementsClass.new(
|
|
308
|
+
identifier: "/req/test",
|
|
309
|
+
normative_statements: [
|
|
310
|
+
Modspec::NormativeStatement.new(identifier: "/req/test/a",
|
|
311
|
+
name: "A", statement: "a"),
|
|
312
|
+
],
|
|
313
|
+
),
|
|
314
|
+
],
|
|
315
|
+
conformance_classes: [
|
|
316
|
+
Modspec::ConformanceClass.new(
|
|
317
|
+
identifier: "/conf/test",
|
|
318
|
+
tests: [
|
|
319
|
+
Modspec::ConformanceTest.new(
|
|
320
|
+
identifier: "/conf/test/a",
|
|
321
|
+
name: "CT-A",
|
|
322
|
+
targets: ["/req/test/a"],
|
|
323
|
+
),
|
|
324
|
+
],
|
|
325
|
+
),
|
|
326
|
+
],
|
|
327
|
+
)
|
|
98
328
|
|
|
99
|
-
|
|
329
|
+
suite.setup_relationships
|
|
330
|
+
ct = suite.conformance_classes.first.tests.first
|
|
331
|
+
expect(ct.corresponding_requirements.map(&:identifier)).to eq(["/req/test/a"])
|
|
332
|
+
expect(ct.parent_class).to eq(suite.conformance_classes.first)
|
|
333
|
+
end
|
|
100
334
|
|
|
101
|
-
|
|
102
|
-
|
|
335
|
+
it "handles missing target gracefully (no matching requirement)" do
|
|
336
|
+
suite = described_class.new(
|
|
337
|
+
identifier: "/suite",
|
|
338
|
+
name: "Test",
|
|
339
|
+
normative_statements_classes: [
|
|
340
|
+
Modspec::NormativeStatementsClass.new(
|
|
341
|
+
identifier: "/req/test",
|
|
342
|
+
normative_statements: [
|
|
343
|
+
Modspec::NormativeStatement.new(identifier: "/req/test/a",
|
|
344
|
+
name: "A", statement: "a"),
|
|
345
|
+
],
|
|
346
|
+
),
|
|
347
|
+
],
|
|
348
|
+
conformance_classes: [
|
|
349
|
+
Modspec::ConformanceClass.new(
|
|
350
|
+
identifier: "/conf/test",
|
|
351
|
+
tests: [
|
|
352
|
+
Modspec::ConformanceTest.new(
|
|
353
|
+
identifier: "/conf/test/a",
|
|
354
|
+
name: "CT-A",
|
|
355
|
+
targets: ["/req/test/missing"],
|
|
356
|
+
),
|
|
357
|
+
],
|
|
358
|
+
),
|
|
359
|
+
],
|
|
360
|
+
)
|
|
103
361
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
expect(
|
|
107
|
-
|
|
362
|
+
suite.setup_relationships
|
|
363
|
+
ct = suite.conformance_classes.first.tests.first
|
|
364
|
+
expect(ct.corresponding_requirements).to be_empty
|
|
365
|
+
end
|
|
108
366
|
|
|
109
|
-
|
|
110
|
-
|
|
367
|
+
it "returns early when conformance_classes is nil" do
|
|
368
|
+
suite = described_class.new(
|
|
369
|
+
identifier: "/suite",
|
|
370
|
+
name: "Test",
|
|
371
|
+
normative_statements_classes: [
|
|
372
|
+
Modspec::NormativeStatementsClass.new(
|
|
373
|
+
identifier: "/req/test",
|
|
374
|
+
normative_statements: [],
|
|
375
|
+
),
|
|
376
|
+
],
|
|
377
|
+
conformance_classes: nil,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
expect { suite.setup_relationships }.not_to raise_error
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
describe "#resolve_conflicts" do
|
|
385
|
+
let(:nsc_with_data) do
|
|
386
|
+
Modspec::NormativeStatementsClass.new(
|
|
387
|
+
identifier: "/req/test",
|
|
388
|
+
name: "Original",
|
|
389
|
+
description: "First description",
|
|
390
|
+
normative_statements: [
|
|
391
|
+
Modspec::NormativeStatement.new(identifier: "/req/test/a",
|
|
392
|
+
name: "A", statement: "a"),
|
|
393
|
+
],
|
|
394
|
+
)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
let(:nsc_partial) do
|
|
398
|
+
Modspec::NormativeStatementsClass.new(
|
|
399
|
+
identifier: "/req/test",
|
|
400
|
+
name: "Original",
|
|
401
|
+
description: "Second description",
|
|
402
|
+
subject: "Added subject",
|
|
403
|
+
normative_statements: [],
|
|
404
|
+
)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
let(:nsc_other) do
|
|
408
|
+
Modspec::NormativeStatementsClass.new(
|
|
409
|
+
identifier: "/req/test2",
|
|
410
|
+
name: "Two",
|
|
411
|
+
normative_statements: [
|
|
412
|
+
Modspec::NormativeStatement.new(identifier: "/req/test2/b",
|
|
413
|
+
name: "B", statement: "b"),
|
|
414
|
+
],
|
|
415
|
+
)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
it "merges attributes of items with matching identifiers" do
|
|
419
|
+
suite1 = described_class.new(
|
|
420
|
+
identifier: "/suite", name: "Test1",
|
|
421
|
+
normative_statements_classes: [nsc_with_data],
|
|
422
|
+
conformance_classes: []
|
|
423
|
+
)
|
|
424
|
+
suite2 = described_class.new(
|
|
425
|
+
identifier: "/suite", name: "Test2",
|
|
426
|
+
normative_statements_classes: [nsc_partial],
|
|
427
|
+
conformance_classes: []
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
suite1.resolve_conflicts(suite2)
|
|
431
|
+
nsc = suite1.normative_statements_classes.first
|
|
432
|
+
expect(nsc.description).to eq("First description")
|
|
433
|
+
expect(nsc.subject).to eq("Added subject")
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
it "appends items with new identifiers" do
|
|
437
|
+
suite1 = described_class.new(
|
|
438
|
+
identifier: "/suite", name: "Test1",
|
|
439
|
+
normative_statements_classes: [nsc_with_data],
|
|
440
|
+
conformance_classes: []
|
|
441
|
+
)
|
|
442
|
+
suite2 = described_class.new(
|
|
443
|
+
identifier: "/suite", name: "Test2",
|
|
444
|
+
normative_statements_classes: [nsc_other],
|
|
445
|
+
conformance_classes: []
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
suite1.resolve_conflicts(suite2)
|
|
449
|
+
expect(suite1.normative_statements_classes.map(&:identifier)).to eq(
|
|
450
|
+
["/req/test", "/req/test2"],
|
|
451
|
+
)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
describe "YAML round-trip" do
|
|
456
|
+
it "serializes and deserializes a normative statements class" do
|
|
457
|
+
original = described_class.from_yaml(rc_yaml)
|
|
458
|
+
yaml_out = original.to_yaml
|
|
459
|
+
round_tripped = described_class.from_yaml(yaml_out)
|
|
460
|
+
|
|
461
|
+
expect(round_tripped.identifier).to eq(original.identifier)
|
|
462
|
+
expect(round_tripped.name).to eq(original.name)
|
|
463
|
+
expect(round_tripped.normative_statements_classes.count).to eq(
|
|
464
|
+
original.normative_statements_classes.count,
|
|
465
|
+
)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
it "serializes and deserializes a conformance class" do
|
|
469
|
+
original = described_class.from_yaml(cc_yaml)
|
|
470
|
+
yaml_out = original.to_yaml
|
|
471
|
+
round_tripped = described_class.from_yaml(yaml_out)
|
|
472
|
+
|
|
473
|
+
expect(round_tripped.identifier).to eq(original.identifier)
|
|
474
|
+
expect(round_tripped.conformance_classes.count).to eq(
|
|
475
|
+
original.conformance_classes.count,
|
|
476
|
+
)
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
describe "JSON round-trip" do
|
|
481
|
+
let(:json_rc) { File.read("spec/fixtures/basic-ypr-json-rc.yaml") }
|
|
482
|
+
let(:json_cc) { File.read("spec/fixtures/basic-ypr-json-cc.yaml") }
|
|
483
|
+
|
|
484
|
+
it "parses JSON-format YAML fixtures" do
|
|
485
|
+
suite = described_class.from_yaml(json_rc)
|
|
486
|
+
expect(suite.normative_statements_classes).not_to be_empty
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
it "round-trips through JSON serialization" do
|
|
490
|
+
original = described_class.from_yaml(rc_yaml)
|
|
491
|
+
json_out = original.to_json
|
|
492
|
+
round_tripped = described_class.from_json(json_out)
|
|
493
|
+
|
|
494
|
+
expect(round_tripped.identifier).to eq(original.identifier)
|
|
495
|
+
expect(round_tripped.name).to eq(original.name)
|
|
111
496
|
end
|
|
112
497
|
end
|
|
113
498
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: modspec
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -71,6 +71,7 @@ files:
|
|
|
71
71
|
- README.adoc
|
|
72
72
|
- Rakefile
|
|
73
73
|
- lib/modspec.rb
|
|
74
|
+
- lib/modspec/child_container.rb
|
|
74
75
|
- lib/modspec/conformance_class.rb
|
|
75
76
|
- lib/modspec/conformance_test.rb
|
|
76
77
|
- lib/modspec/identifier.rb
|