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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7a4558cdeaa90d0b4197daca351b9eadbeee5af21cb8454e9a1f44b3725bbaa
4
- data.tar.gz: d0134aeca43dd6ee3415bfafe25ea7f67212eda1b8577a3bdfd9d9a3800aa150
3
+ metadata.gz: 2403a7e55f95df5cf1821375442902c0a06784c26186f1e2fc1ae2e474ee937b
4
+ data.tar.gz: 4cbb3354c710f6d6e19114b2e2c28e533251c10bfa4a93c0a917ff73e5ebce93
5
5
  SHA512:
6
- metadata.gz: 70827aadfd85037c992e670923ae4c7065c6ba913905028b903f4b6535f8498f530d3ec51a9ff245aa9734a3f044d6ca779b5e8b849b0a15d1b6b2aaed360e2f
7
- data.tar.gz: 9ba46d5f8efa5a57e4b23c2557969cdd5c6b6697cb8584bb98b7c11fc237a099743c33474b46d05444361785278707981206af09e7c32570261ae5b104a9885d
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],
@@ -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
- 'Always pass --test-type when a surface has more than one suite, or they overwrite each other.'
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
- params[:pr_url] = opts[:pr] if opts[:pr]
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
- puts " #{marker} #{r[:binding].slug} → #{r[:binding].surface}#{type} (#{shown_state}) #{loc}"
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)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fp
4
- VERSION = '0.0.25'
4
+ VERSION = '0.0.26'
5
5
  end
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.25
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-23 00:00:00.000000000 Z
11
+ date: 2026-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-http