featureparity 0.0.25 → 0.0.26
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/client.rb +4 -1
- data/lib/fp/commands/ci_report.rb +1 -0
- data/lib/fp/commands/report.rb +112 -9
- data/lib/fp/commands/show.rb +9 -0
- data/lib/fp/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2403a7e55f95df5cf1821375442902c0a06784c26186f1e2fc1ae2e474ee937b
|
|
4
|
+
data.tar.gz: 4cbb3354c710f6d6e19114b2e2c28e533251c10bfa4a93c0a917ff73e5ebce93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a065b839f21e0bdf4194f65b93c233abf3e09ef11a50f96d0259d68609fc1c6e89e3d07e760a5a8d3b3bb12b849b7fdb7ea1c3a5fa65fc59b4befa053595366
|
|
7
|
+
data.tar.gz: 50e977bdf9a66c6f24a8a264d9334a506d5f15379f8e9aeaa8635d6cf84b5f9eee526e990c6e9b1a041631f1e15b7115e3ed3484311130f1fd23ca0ad7d35cde
|
data/lib/fp/client.rb
CHANGED
|
@@ -281,7 +281,7 @@ module Fp
|
|
|
281
281
|
# only do that by fetching the test files from GitHub, which fails silently on a
|
|
282
282
|
# private repo, so resolved results are always sent when we have them.
|
|
283
283
|
def ingest_ci_results(project_id, ci_url:, surface:, junit_xml:, pr_url: nil, results: nil,
|
|
284
|
-
test_revisions: nil, unmarked_failures: nil, orphan_markers: nil)
|
|
284
|
+
test_revisions: nil, unmarked_failures: nil, orphan_markers: nil, sha: nil)
|
|
285
285
|
body = {
|
|
286
286
|
ci_url: ci_url,
|
|
287
287
|
surface: surface,
|
|
@@ -289,6 +289,9 @@ module Fp
|
|
|
289
289
|
}
|
|
290
290
|
# Only send pr_url when set, so a merged-branch run stays a plain ingest.
|
|
291
291
|
body[:pr_url] = pr_url if pr_url && !pr_url.empty?
|
|
292
|
+
# The checkout commit, so the receipt (or a PR run's branch verdict) links the
|
|
293
|
+
# exact revision the suite ran at.
|
|
294
|
+
body[:sha] = sha if sha && !sha.empty?
|
|
292
295
|
body[:results] = results if results && !results.empty?
|
|
293
296
|
# Testcase-level source receipts stay separate from aggregate Evidence outcomes:
|
|
294
297
|
# several tests may back one cell, and each can change independently.
|
|
@@ -146,6 +146,7 @@ module Fp
|
|
|
146
146
|
# with servers old enough to ignore client-resolved results.
|
|
147
147
|
junit_xml: resolved_reports.first[:xml],
|
|
148
148
|
pr_url: opts[:pr],
|
|
149
|
+
sha: commit_sha,
|
|
149
150
|
results: combined_results,
|
|
150
151
|
test_revisions: combined_revisions,
|
|
151
152
|
unmarked_failures: totals[:unmarked_failures],
|
data/lib/fp/commands/report.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'set'
|
|
3
5
|
require_relative 'attach'
|
|
4
6
|
|
|
5
7
|
module Fp
|
|
@@ -37,11 +39,14 @@ module Fp
|
|
|
37
39
|
help_flag :junit, 'JUnit XML report to batch-upload evidence from'
|
|
38
40
|
help_flag :base_dir, 'Where relative test-file paths in the report resolve from'
|
|
39
41
|
help_flag :attach_media, 'With --junit: upload the videos/screenshots the run recorded ([[ATTACHMENT|…]]) as visual evidence'
|
|
42
|
+
help_flag :stamp_all, 'With --junit: put --pr/--work-item on every receipt, not only on tests in files this branch changed'
|
|
40
43
|
help_example 'fp report print_container_qr --project stowzilla --surface api --file spec/qr_spec.rb --repo stowzilla/app'
|
|
41
44
|
help_example 'fp report --junit junit.xml --project stowzilla --surface api --repo stowzilla/app',
|
|
42
45
|
'Batch-upload from a JUnit report (slugs come from fp:<slug> markers)'
|
|
43
46
|
help_note "Agents report 'present' or 'stub' only. CI writes 'passing'/'failing' via fp ci-report.\n" \
|
|
44
|
-
|
|
47
|
+
"Always pass --test-type when a surface has more than one suite, or they overwrite each other.\n" \
|
|
48
|
+
'With --junit, --pr and --work-item land only on tests in files this branch changed (git); ' \
|
|
49
|
+
'the rest of the suite keeps the card and PR that introduced it.'
|
|
45
50
|
|
|
46
51
|
KNOWN_FLAGS = {
|
|
47
52
|
project: :string,
|
|
@@ -58,7 +63,8 @@ module Fp
|
|
|
58
63
|
state: :string,
|
|
59
64
|
junit: :string,
|
|
60
65
|
base_dir: :string,
|
|
61
|
-
attach_media: :boolean
|
|
66
|
+
attach_media: :boolean,
|
|
67
|
+
stamp_all: :boolean
|
|
62
68
|
}.freeze
|
|
63
69
|
|
|
64
70
|
VALID_STATES = %w[present stub].freeze
|
|
@@ -261,6 +267,12 @@ module Fp
|
|
|
261
267
|
|
|
262
268
|
project_id = resolve_project_id(opts[:project])
|
|
263
269
|
|
|
270
|
+
# --pr and --work-item say what prompted *this* work. A suite run covers every
|
|
271
|
+
# marker in the repo, so stamping them on every receipt rewrote the card and PR
|
|
272
|
+
# on requirements the branch never touched — "View Card" on an unrelated
|
|
273
|
+
# requirement led to whichever card last ran the suite (Fizzy #1631). Only the
|
|
274
|
+
# tests in files this branch changed get them; the rest keep their own.
|
|
275
|
+
provenance = provenance_scope(opts, base_dir)
|
|
264
276
|
reported = []
|
|
265
277
|
skipped_unknown = []
|
|
266
278
|
api_unavailable = false
|
|
@@ -289,9 +301,10 @@ module Fp
|
|
|
289
301
|
}
|
|
290
302
|
params[:test_type] = binding.test_type if binding.test_type
|
|
291
303
|
params[:line] = binding.line if binding.line
|
|
292
|
-
|
|
304
|
+
stamped = provenance.stamps?(binding.file)
|
|
305
|
+
params[:pr_url] = opts[:pr] if opts[:pr] && stamped
|
|
293
306
|
params[:sha] = opts[:sha] if opts[:sha]
|
|
294
|
-
params[:work_item_url] = opts[:work_item] if opts[:work_item]
|
|
307
|
+
params[:work_item_url] = opts[:work_item] if opts[:work_item] && stamped
|
|
295
308
|
params[:ci_url] = opts[:ci_url] if opts[:ci_url]
|
|
296
309
|
params[:title] = binding.title if binding.title
|
|
297
310
|
|
|
@@ -310,7 +323,7 @@ module Fp
|
|
|
310
323
|
exit 1
|
|
311
324
|
end
|
|
312
325
|
|
|
313
|
-
reported << { binding: binding, state: state, recorded: true,
|
|
326
|
+
reported << { binding: binding, state: state, recorded: true, stamped: stamped,
|
|
314
327
|
stored_status: stored_status_from(report_result),
|
|
315
328
|
stored_source: stored_source_from(report_result) }
|
|
316
329
|
end
|
|
@@ -321,7 +334,80 @@ module Fp
|
|
|
321
334
|
end
|
|
322
335
|
|
|
323
336
|
emit_junit_summary(opts, reported, skipped_unknown, unmatched, api_unavailable, collisions,
|
|
324
|
-
empty_name: empty_name, media: media, uploaded_media: uploaded_media
|
|
337
|
+
empty_name: empty_name, media: media, uploaded_media: uploaded_media,
|
|
338
|
+
provenance: provenance)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Which receipts of a --junit batch carry --pr/--work-item.
|
|
342
|
+
#
|
|
343
|
+
# `files` is the set of repo-relative paths this branch changed — the commits since
|
|
344
|
+
# it left the default branch, plus anything uncommitted or untracked — or, when
|
|
345
|
+
# the checkout *is* the default branch, the files in its HEAD commit. Nil means
|
|
346
|
+
# git couldn't say (not a repository, no default branch to compare with), and then
|
|
347
|
+
# every receipt is stamped as before, with a note. `--stamp-all` forces that.
|
|
348
|
+
ProvenanceScope = Struct.new(:mode, :files, keyword_init: true) do
|
|
349
|
+
def stamps?(file)
|
|
350
|
+
return true if mode == :all || files.nil?
|
|
351
|
+
|
|
352
|
+
files.include?(file.to_s)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def scoped?
|
|
356
|
+
mode != :all && !files.nil?
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def provenance_scope(opts, base_dir)
|
|
361
|
+
return ProvenanceScope.new(mode: :all, files: nil) if opts[:stamp_all]
|
|
362
|
+
return ProvenanceScope.new(mode: :all, files: nil) if opts[:pr].nil? && opts[:work_item].nil?
|
|
363
|
+
|
|
364
|
+
root = Fp::JUnit.repo_root_for(base_dir)
|
|
365
|
+
return ProvenanceScope.new(mode: :no_git, files: nil) if root.nil?
|
|
366
|
+
|
|
367
|
+
head = git_out(root, 'rev-parse', 'HEAD')
|
|
368
|
+
return ProvenanceScope.new(mode: :no_git, files: nil) if head.nil? || head.empty?
|
|
369
|
+
|
|
370
|
+
base = merge_base_with_default_branch(root)
|
|
371
|
+
committed =
|
|
372
|
+
if base && base != head
|
|
373
|
+
git_out(root, 'diff', '--name-only', base, 'HEAD')
|
|
374
|
+
elsif base
|
|
375
|
+
# On the default branch itself: the work is whatever this commit touched.
|
|
376
|
+
git_out(root, 'diff-tree', '--no-commit-id', '--name-only', '-r', '--root', 'HEAD')
|
|
377
|
+
end
|
|
378
|
+
return ProvenanceScope.new(mode: :no_git, files: nil) if committed.nil?
|
|
379
|
+
|
|
380
|
+
pending = git_out(root, 'diff', '--name-only', 'HEAD') || ''
|
|
381
|
+
untracked = git_out(root, 'ls-files', '--others', '--exclude-standard') || ''
|
|
382
|
+
files = [committed, pending, untracked].join("\n").split("\n").map(&:strip).reject(&:empty?).to_set
|
|
383
|
+
ProvenanceScope.new(mode: (base != head ? :branch : :commit), files: files)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# The merge base between HEAD and the default branch, trying origin's HEAD first
|
|
387
|
+
# and the usual names after it. Nil when none of them exists locally.
|
|
388
|
+
def merge_base_with_default_branch(root)
|
|
389
|
+
candidates = []
|
|
390
|
+
remote_head = git_out(root, 'symbolic-ref', '--short', '-q', 'refs/remotes/origin/HEAD')
|
|
391
|
+
candidates << remote_head if remote_head && !remote_head.empty?
|
|
392
|
+
candidates.concat(%w[origin/main origin/master main master])
|
|
393
|
+
candidates.uniq.each do |ref|
|
|
394
|
+
verified = git_out(root, 'rev-parse', '--verify', '-q', "#{ref}^{commit}")
|
|
395
|
+
next if verified.nil? || verified.empty?
|
|
396
|
+
|
|
397
|
+
base = git_out(root, 'merge-base', ref, 'HEAD')
|
|
398
|
+
return base if base && !base.empty?
|
|
399
|
+
end
|
|
400
|
+
nil
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Stdout of a git command, stripped — '' when it printed nothing, nil on failure.
|
|
404
|
+
def git_out(root, *args)
|
|
405
|
+
stdout, _stderr, status = Open3.capture3('git', *args, chdir: root)
|
|
406
|
+
return nil unless status.success?
|
|
407
|
+
|
|
408
|
+
stdout.strip
|
|
409
|
+
rescue SystemCallError
|
|
410
|
+
nil
|
|
325
411
|
end
|
|
326
412
|
|
|
327
413
|
# Upload the media (videos/screenshots) a run recorded for its bound testcases as
|
|
@@ -397,10 +483,13 @@ module Fp
|
|
|
397
483
|
end
|
|
398
484
|
|
|
399
485
|
def emit_junit_summary(opts, reported, skipped_unknown, unmatched, api_unavailable, collisions = [],
|
|
400
|
-
empty_name: [], media: [], uploaded_media: [])
|
|
486
|
+
empty_name: [], media: [], uploaded_media: [], provenance: nil)
|
|
487
|
+
provenance ||= ProvenanceScope.new(mode: :all, files: nil)
|
|
488
|
+
stamping = !opts[:pr].nil? || !opts[:work_item].nil?
|
|
401
489
|
data = {
|
|
402
490
|
repo: opts[:repo],
|
|
403
|
-
reported: reported.map { |r| { slug: r[:binding].slug, surface: r[:binding].surface, test_type: r[:binding].test_type, file: r[:binding].file, line: r[:binding].line, state: r[:state], recorded: r[:recorded], stored_status: r[:stored_status], superseded_by_ci: ci_superseded?(r[:state], r[:stored_status], r[:stored_source]) } },
|
|
491
|
+
reported: reported.map { |r| { slug: r[:binding].slug, surface: r[:binding].surface, test_type: r[:binding].test_type, file: r[:binding].file, line: r[:binding].line, state: r[:state], recorded: r[:recorded], stamped: (stamping ? r[:stamped] != false : nil), stored_status: r[:stored_status], superseded_by_ci: ci_superseded?(r[:state], r[:stored_status], r[:stored_source]) }.compact },
|
|
492
|
+
provenance: (stamping ? { scoped: provenance.scoped?, mode: provenance.mode.to_s, stamped: reported.count { |r| r[:stamped] != false } } : nil),
|
|
404
493
|
skipped_unknown: skipped_unknown.map { |b| { slug: b.slug, surface: b.surface, file: b.file } },
|
|
405
494
|
unmatched_testcases: unmatched.map { |tc| { name: tc.name, file: tc.file } },
|
|
406
495
|
empty_name: empty_name,
|
|
@@ -423,7 +512,21 @@ module Fp
|
|
|
423
512
|
loc = r[:binding].line ? "#{r[:binding].file}:#{r[:binding].line}" : r[:binding].file
|
|
424
513
|
type = r[:binding].test_type ? " [#{r[:binding].test_type}]" : ''
|
|
425
514
|
shown_state = superseded ? "#{r[:stored_status]}, kept CI — #{r[:state]} ignored" : r[:state]
|
|
426
|
-
|
|
515
|
+
stamp = stamping && provenance.scoped? && r[:stamped] ? ' ← this change' : ''
|
|
516
|
+
puts " #{marker} #{r[:binding].slug} → #{r[:binding].surface}#{type} (#{shown_state}) #{loc}#{stamp}"
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
if stamping
|
|
520
|
+
puts
|
|
521
|
+
stamped = reported.count { |r| r[:stamped] != false }
|
|
522
|
+
what = [opts[:pr] && '--pr', opts[:work_item] && '--work-item'].compact.join(' and ')
|
|
523
|
+
if provenance.scoped?
|
|
524
|
+
where = provenance.mode == :commit ? 'the files in this commit' : 'the files this branch changed'
|
|
525
|
+
puts "ℹ️ #{what} recorded on #{stamped} of #{reported.length} receipt(s) — the tests in #{where}."
|
|
526
|
+
puts ' The rest keep the card and PR that introduced them. Pass --stamp-all to override.'
|
|
527
|
+
elsif provenance.mode == :no_git
|
|
528
|
+
puts "ℹ️ #{what} recorded on every receipt: git could not say which files this change touched."
|
|
529
|
+
end
|
|
427
530
|
end
|
|
428
531
|
|
|
429
532
|
emit_collision_warning(collisions)
|
data/lib/fp/commands/show.rb
CHANGED
|
@@ -164,6 +164,15 @@ module Fp
|
|
|
164
164
|
if e['merged_status'] && e['merged_status'] != e['status']
|
|
165
165
|
puts " Merged: #{e['merged_status']} (the default branch's last verdict)"
|
|
166
166
|
end
|
|
167
|
+
# An open pull request's CI has reported on this test since the default
|
|
168
|
+
# branch did. It never changes the status above (Fizzy #1631); it is
|
|
169
|
+
# the heads-up that a branch says otherwise, with the PR to look at.
|
|
170
|
+
if e['branch_status']
|
|
171
|
+
branch = ["On PR: #{e['branch_status']}"]
|
|
172
|
+
branch << e['branch_pr_url'] if e['branch_pr_url']
|
|
173
|
+
branch << '(not merged)'
|
|
174
|
+
puts " #{branch.join(' ')}"
|
|
175
|
+
end
|
|
167
176
|
puts " Type: #{e['test_type']}" if e['test_type'] && e['test_type'] != 'unspecified'
|
|
168
177
|
if e['source_file']
|
|
169
178
|
loc = e['source_line'] ? "#{e['source_file']}:#{e['source_line']}" : e['source_file']
|
data/lib/fp/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: featureparity
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.26
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: net-http
|