featureparity 0.0.27 → 0.0.29
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/lib/fp/cli.rb +14 -2
- data/lib/fp/client.rb +79 -2
- data/lib/fp/commands/analyze.rb +17 -0
- data/lib/fp/commands/base.rb +16 -0
- data/lib/fp/commands/check.rb +38 -4
- data/lib/fp/commands/ci_report.rb +2 -2
- data/lib/fp/commands/comment.rb +1 -1
- data/lib/fp/commands/comments.rb +146 -0
- data/lib/fp/commands/coverage.rb +149 -0
- data/lib/fp/commands/doctor.rb +228 -0
- data/lib/fp/commands/help.rb +41 -1
- data/lib/fp/commands/history.rb +16 -1
- data/lib/fp/commands/init.rb +181 -117
- data/lib/fp/commands/list.rb +58 -4
- data/lib/fp/commands/mark.rb +167 -0
- data/lib/fp/commands/projects.rb +6 -1
- data/lib/fp/commands/propose.rb +435 -53
- data/lib/fp/commands/propose_change.rb +13 -7
- data/lib/fp/commands/question.rb +137 -0
- data/lib/fp/commands/report.rb +40 -4
- data/lib/fp/commands/run.rb +147 -0
- data/lib/fp/commands/show.rb +41 -3
- data/lib/fp/commands/spot_check.rb +458 -0
- data/lib/fp/commands/tutorial.rb +95 -0
- data/lib/fp/commands/whoami.rb +2 -0
- data/lib/fp/commands.rb +8 -0
- data/lib/fp/config.rb +12 -0
- data/lib/fp/junit.rb +74 -11
- data/lib/fp/repo_detector.rb +488 -0
- data/lib/fp/requirement_lint.rb +105 -0
- data/lib/fp/rspec_json.rb +54 -0
- data/lib/fp/tutorial/hexhaven/AGENTS.md +5 -0
- data/lib/fp/tutorial/hexhaven/README.md +29 -0
- data/lib/fp/tutorial/hexhaven/lib/hexhaven.rb +36 -0
- data/lib/fp/tutorial/hexhaven/test/junit_reporter.rb +134 -0
- data/lib/fp/tutorial/hexhaven/test/rules_test.rb +36 -0
- data/lib/fp/tutorial/hexhaven/test/test_helper.rb +6 -0
- data/lib/fp/version.rb +1 -1
- data/lib/fp.rb +1 -0
- data/skills/fp/SKILL.md +244 -105
- data/skills/fp/references/cli.md +304 -3
- metadata +19 -2
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fp
|
|
4
|
+
module Commands
|
|
5
|
+
# fp mark <slug> <file>:<line> [--surface a,b] [--type e2e] [--comment '#'] [--dry-run]
|
|
6
|
+
#
|
|
7
|
+
# Insert an fp:<slug> marker directly above the test at <file>:<line>, in that
|
|
8
|
+
# file's own comment syntax and indentation (Fizzy #1786). Marking by hand across
|
|
9
|
+
# Ruby, JS, Swift and SQL meant a script per language and the occasional marker the
|
|
10
|
+
# scanner didn't recognise; this writes one the scanner reads back, and says which
|
|
11
|
+
# test it binds to. Purely local: no API key, no network.
|
|
12
|
+
class Mark < Base
|
|
13
|
+
help_summary 'Insert an fp:<slug> marker above a test, in the file\'s own comment syntax'
|
|
14
|
+
help_usage 'fp mark <slug> <file>:<line> [--surface a,b] [--type <test-type>] [--dry-run]'
|
|
15
|
+
help_flag :surface, 'Pin the marker to these surfaces (fp:slug@a,b); omit to use --surface at report time'
|
|
16
|
+
help_flag :type, 'Pin the kind of test (fp:slug@surface:e2e); needs --surface'
|
|
17
|
+
help_flag :comment, "Comment prefix to use when the extension isn't known (e.g. '#', '//', '--')"
|
|
18
|
+
help_flag :dry_run, 'Show the marker and where it would go; change nothing'
|
|
19
|
+
help_example 'fp mark academic_experience_required_fields spec/models/academic_experience_spec.rb:52'
|
|
20
|
+
help_example 'fp mark checkout_flow e2e/checkout.spec.ts:10 --surface web --type e2e'
|
|
21
|
+
help_note "<line> is the test's own line (the `it`/`test`/`def test_` line); the marker goes directly above it.\n" \
|
|
22
|
+
'Marking a test binds it to a requirement: never mark a test that is currently failing to make it count.'
|
|
23
|
+
|
|
24
|
+
KNOWN_FLAGS = {
|
|
25
|
+
surface: :string,
|
|
26
|
+
type: :string,
|
|
27
|
+
comment: :string,
|
|
28
|
+
dry_run: :boolean
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
COMMENT_PREFIXES = {
|
|
32
|
+
'#' => %w[rb rake gemspec ru py sh bash zsh yml yaml toml ex exs r pl cr jl nim tf],
|
|
33
|
+
'//' => %w[js jsx mjs cjs ts tsx mts cts swift kt kts java go rs c cc cpp cxx h hpp m mm cs dart
|
|
34
|
+
scala php groovy gradle zig v],
|
|
35
|
+
'--' => %w[sql lua hs elm]
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
# Files named without an extension that are Ruby.
|
|
39
|
+
RUBY_FILENAMES = %w[Rakefile Gemfile Guardfile Podfile Fastfile].freeze
|
|
40
|
+
|
|
41
|
+
SLUG_RE = /\A[a-z0-9][a-z0-9_-]*\z/.freeze
|
|
42
|
+
KEY_RE = /\A[a-z0-9_-]+\z/.freeze
|
|
43
|
+
|
|
44
|
+
def run(args)
|
|
45
|
+
opts, positional = parse_flags(args, KNOWN_FLAGS)
|
|
46
|
+
slug, location = positional
|
|
47
|
+
|
|
48
|
+
unless slug && location
|
|
49
|
+
output.error('Usage: fp mark <slug> <file>:<line>')
|
|
50
|
+
exit 1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
fail_with("'#{slug}' isn't a valid slug (lowercase letters, numbers, - and _).") unless slug.match?(SLUG_RE)
|
|
54
|
+
|
|
55
|
+
path, line_no = parse_location(location)
|
|
56
|
+
surfaces = (opts[:surface] || '').split(',').map(&:strip).reject(&:empty?)
|
|
57
|
+
bad = surfaces.reject { |s| s.match?(KEY_RE) }
|
|
58
|
+
fail_with("Invalid surface key(s): #{bad.join(', ')}") unless bad.empty?
|
|
59
|
+
if opts[:type]
|
|
60
|
+
fail_with("Invalid test type '#{opts[:type]}'") unless opts[:type].match?(KEY_RE)
|
|
61
|
+
fail_with('--type needs --surface: a marker pins its kind after its surfaces (fp:slug@api:e2e)') if surfaces.empty?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
prefix = opts[:comment] || comment_prefix_for(path)
|
|
65
|
+
unless prefix
|
|
66
|
+
fail_with("Don't know the comment syntax for #{File.basename(path)}. Pass --comment, e.g. --comment '#' or --comment '//'.")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
lines = File.read(path).lines
|
|
70
|
+
if line_no > lines.length
|
|
71
|
+
fail_with("#{path} has #{lines.length} line(s); there is no line #{line_no}.")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
marker = "fp:#{slug}"
|
|
75
|
+
marker += "@#{surfaces.join(',')}" unless surfaces.empty?
|
|
76
|
+
marker += ":#{opts[:type]}" if opts[:type]
|
|
77
|
+
|
|
78
|
+
target = lines[line_no - 1]
|
|
79
|
+
indent = target[/\A[ \t]*/]
|
|
80
|
+
comment_line = "#{indent}#{prefix} #{marker}#{line_ending(target)}"
|
|
81
|
+
|
|
82
|
+
if already_marked?(lines, line_no, marker)
|
|
83
|
+
return report(path, line_no, marker, nil, already: true)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
updated = lines.dup.insert(line_no - 1, comment_line)
|
|
87
|
+
found = Fp::JUnit.scan_markers(updated.join).find { |m| m[:line] == line_no && m[:slug] == slug }
|
|
88
|
+
unless found
|
|
89
|
+
fail_with("A '#{prefix}' comment isn't one fp's scanner reads as a marker. Use #, //, -- or /* … */.")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
File.write(path, updated.join) unless opts[:dry_run]
|
|
93
|
+
report(path, line_no, marker, found, dry_run: opts[:dry_run], comment_line: comment_line.chomp)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def parse_location(location)
|
|
99
|
+
m = location.match(/\A(.+):(\d+)\z/)
|
|
100
|
+
fail_with("Give the test's location as <file>:<line> (got '#{location}').") unless m
|
|
101
|
+
|
|
102
|
+
path = m[1]
|
|
103
|
+
line_no = m[2].to_i
|
|
104
|
+
fail_with("No such file: #{path}") unless File.file?(path)
|
|
105
|
+
fail_with('Line numbers start at 1.') if line_no < 1
|
|
106
|
+
|
|
107
|
+
[path, line_no]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def comment_prefix_for(path)
|
|
111
|
+
base = File.basename(path)
|
|
112
|
+
return '#' if RUBY_FILENAMES.include?(base)
|
|
113
|
+
|
|
114
|
+
ext = File.extname(base).delete('.').downcase
|
|
115
|
+
COMMENT_PREFIXES.find { |_, exts| exts.include?(ext) }&.first
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def line_ending(line)
|
|
119
|
+
line.end_with?("\r\n") ? "\r\n" : "\n"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The same marker already sits in the comment block directly above the test.
|
|
123
|
+
def already_marked?(lines, line_no, marker)
|
|
124
|
+
i = line_no - 2
|
|
125
|
+
while i >= 0 && lines[i].match?(Fp::JUnit::COMMENT_LINE_RE)
|
|
126
|
+
return true if lines[i].scan(Fp::JUnit::MARKER_RE).any? { |m| marker_string(m) == marker }
|
|
127
|
+
|
|
128
|
+
i -= 1
|
|
129
|
+
end
|
|
130
|
+
false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def marker_string((slug, surfaces, type))
|
|
134
|
+
s = "fp:#{slug}"
|
|
135
|
+
s += "@#{surfaces}" if surfaces
|
|
136
|
+
s += ":#{type}" if type
|
|
137
|
+
s
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def report(path, line_no, marker, found, already: false, dry_run: false, comment_line: nil)
|
|
141
|
+
data = { file: path, line: line_no, marker: marker, already_marked: already, dry_run: dry_run,
|
|
142
|
+
binds_to: found && found[:title] }.compact
|
|
143
|
+
output.success(data) do
|
|
144
|
+
if already
|
|
145
|
+
puts "#{path}:#{line_no} is already marked #{marker}; nothing changed."
|
|
146
|
+
next
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
verb = dry_run ? 'Would insert' : 'Inserted'
|
|
150
|
+
puts "#{verb} at #{path}:#{line_no}:"
|
|
151
|
+
puts " #{comment_line.strip}"
|
|
152
|
+
if found[:title]
|
|
153
|
+
puts "It binds to: #{found[:title]}"
|
|
154
|
+
else
|
|
155
|
+
puts "⚠️ No test declaration was recognised below line #{line_no}. Check that the line is the test itself;"
|
|
156
|
+
puts ' if the runner reports file paths, the marker still binds by file.'
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def fail_with(message)
|
|
162
|
+
output.error(message)
|
|
163
|
+
exit 1
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
data/lib/fp/commands/projects.rb
CHANGED
|
@@ -87,7 +87,11 @@ module Fp
|
|
|
87
87
|
output.success(result[:data]) do
|
|
88
88
|
if projects.empty?
|
|
89
89
|
puts 'No projects found.'
|
|
90
|
-
|
|
90
|
+
if (hint = config&.environment_mismatch_hint)
|
|
91
|
+
puts hint
|
|
92
|
+
else
|
|
93
|
+
puts 'Create one with: fp projects create <slug> --name "<Name>"'
|
|
94
|
+
end
|
|
91
95
|
else
|
|
92
96
|
headers = %w[SLUG NAME SURFACES]
|
|
93
97
|
rows = projects.map do |w|
|
|
@@ -132,6 +136,7 @@ module Fp
|
|
|
132
136
|
surfaces = split_list(opts[:surfaces])
|
|
133
137
|
params[:surfaces] = surfaces unless surfaces.empty?
|
|
134
138
|
|
|
139
|
+
warn_if_not_production!
|
|
135
140
|
result = client.create_project(params)
|
|
136
141
|
|
|
137
142
|
unless result[:ok]
|