hashira 0.2.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +141 -0
- data/README.md +53 -5
- data/lib/hashira/analysis/catalog.rb +27 -0
- data/lib/hashira/analysis/census.rb +42 -35
- data/lib/hashira/analysis/constant_registry.rb +43 -0
- data/lib/hashira/analysis/cycle_findings.rb +34 -29
- data/lib/hashira/analysis/cycle_search.rb +28 -35
- data/lib/hashira/analysis/cycles.rb +14 -0
- data/lib/hashira/analysis/definition.rb +22 -0
- data/lib/hashira/analysis/definitions.rb +22 -18
- data/lib/hashira/analysis/edge.rb +4 -3
- data/lib/hashira/analysis/edge_map.rb +23 -26
- data/lib/hashira/analysis/finding.rb +7 -6
- data/lib/hashira/analysis/folder_placement.rb +17 -0
- data/lib/hashira/analysis/folding.rb +54 -0
- data/lib/hashira/analysis/graph.rb +35 -38
- data/lib/hashira/analysis/metric.rb +8 -7
- data/lib/hashira/analysis/namespace_placement.rb +28 -0
- data/lib/hashira/analysis/namespace_prefix.rb +32 -0
- data/lib/hashira/analysis/naming.rb +14 -0
- data/lib/hashira/analysis/no_folding.rb +13 -0
- data/lib/hashira/analysis/node_walk.rb +3 -3
- data/lib/hashira/analysis/placement.rb +19 -0
- data/lib/hashira/analysis/references.rb +14 -13
- data/lib/hashira/analysis/resolver.rb +28 -0
- data/lib/hashira/analysis/roster.rb +25 -0
- data/lib/hashira/analysis/rule.rb +10 -14
- data/lib/hashira/analysis/sdp_check.rb +8 -12
- data/lib/hashira/analysis/sdp_violation_findings.rb +20 -18
- data/lib/hashira/analysis/syntax.rb +21 -6
- data/lib/hashira/analysis/type_walk.rb +7 -9
- data/lib/hashira/churn.rb +11 -13
- data/lib/hashira/ci/accepted.rb +27 -31
- data/lib/hashira/ci/baseline.rb +22 -24
- data/lib/hashira/ci/diff.rb +8 -7
- data/lib/hashira/ci/edge_diff_report.rb +13 -17
- data/lib/hashira/ci/finding_diff_report.rb +14 -18
- data/lib/hashira/ci/gate.rb +19 -23
- data/lib/hashira/ci/improvement.rb +9 -14
- data/lib/hashira/ci/ratchet.rb +36 -27
- data/lib/hashira/ci/ratchet_report.rb +26 -32
- data/lib/hashira/cli/arguments.rb +23 -0
- data/lib/hashira/cli/command_line.rb +55 -71
- data/lib/hashira/cli/fail_on.rb +15 -20
- data/lib/hashira/cli/options.rb +9 -4
- data/lib/hashira/cli/package_by.rb +14 -0
- data/lib/hashira/cli/run.rb +29 -27
- data/lib/hashira/cli/skip.rb +14 -20
- data/lib/hashira/cli/usage.rb +41 -41
- data/lib/hashira/cli.rb +17 -19
- data/lib/hashira/complexity/analyzer.rb +30 -32
- data/lib/hashira/complexity/boolean_run.rb +11 -15
- data/lib/hashira/complexity/cognitive_score.rb +61 -66
- data/lib/hashira/complexity/if_chain.rb +33 -38
- data/lib/hashira/complexity/method_finding.rb +43 -48
- data/lib/hashira/complexity/method_score.rb +9 -7
- data/lib/hashira/complexity/rescue_scan.rb +16 -21
- data/lib/hashira/complexity/rollup.rb +13 -15
- data/lib/hashira/diagram/dot.rb +8 -11
- data/lib/hashira/diagram/mermaid.rb +11 -16
- data/lib/hashira/diagram/renderer.rb +10 -15
- data/lib/hashira/duplication/analyzer.rb +10 -14
- data/lib/hashira/duplication/cluster.rb +15 -12
- data/lib/hashira/duplication/clusterer.rb +31 -29
- data/lib/hashira/duplication/delta.rb +29 -30
- data/lib/hashira/duplication/duplication_finding.rb +19 -21
- data/lib/hashira/duplication/extractor.rb +15 -19
- data/lib/hashira/duplication/fragment.rb +19 -23
- data/lib/hashira/duplication/grouping.rb +12 -16
- data/lib/hashira/duplication/index.rb +20 -24
- data/lib/hashira/duplication/maximal.rb +11 -15
- data/lib/hashira/duplication/near_miss.rb +19 -22
- data/lib/hashira/duplication/sequence.rb +20 -22
- data/lib/hashira/duplication/similarity.rb +28 -35
- data/lib/hashira/duplication/union_find.rb +10 -14
- data/lib/hashira/duplication/variance.rb +36 -44
- data/lib/hashira/error.rb +4 -2
- data/lib/hashira/hotspots/file_cost.rb +8 -7
- data/lib/hashira/hotspots/rollup.rb +19 -21
- data/lib/hashira/pipeline.rb +36 -31
- data/lib/hashira/project.rb +53 -32
- data/lib/hashira/report/complexity_table.rb +27 -31
- data/lib/hashira/report/dependency_map.rb +17 -22
- data/lib/hashira/report/finding_lines.rb +15 -19
- data/lib/hashira/report/graph_payload.rb +14 -20
- data/lib/hashira/report/hotspot_table.rb +32 -37
- data/lib/hashira/report/json.rb +30 -32
- data/lib/hashira/report/metrics_table.rb +46 -33
- data/lib/hashira/report/text.rb +59 -52
- data/lib/hashira/version.rb +1 -1
- data/lib/hashira.rb +92 -55
- metadata +16 -2
- data/lib/hashira/analysis/root_namespace.rb +0 -14
|
@@ -7,19 +7,17 @@ module Hashira
|
|
|
7
7
|
module TypeWalk
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
def
|
|
11
|
-
return
|
|
12
|
-
|
|
13
|
-
full = prefix + Syntax.path_segments(node.constant_path)
|
|
10
|
+
def each(node, stack = [], roots: nil, &)
|
|
11
|
+
return descend(node, stack, roots, &) unless type?(node)
|
|
12
|
+
full = Syntax.anchor(stack, Syntax.segments(node.constant_path), roots)
|
|
14
13
|
yield(node, full)
|
|
15
|
-
|
|
14
|
+
descend(node.body, stack + [full], roots, &)
|
|
16
15
|
end
|
|
17
16
|
|
|
18
|
-
def
|
|
17
|
+
def type?(node) = node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
|
|
19
18
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
children.each { each_definition(it, prefix, &) }
|
|
19
|
+
def descend(node, stack, roots, &)
|
|
20
|
+
(node ? node.compact_child_nodes : []).each { each(it, stack, roots:, &) }
|
|
23
21
|
end
|
|
24
22
|
end
|
|
25
23
|
end
|
data/lib/hashira/churn.rb
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
SITES_THAT_DRIFT_APART = 2
|
|
3
|
+
class Hashira::Churn
|
|
4
|
+
LOG = "git log --name-only --format= 2>/dev/null"
|
|
5
|
+
SITES_THAT_DRIFT_APART = 2
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
def self.scan = new(tally(`#{LOG}`))
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
def self.tally(output) = output.split("\n").map(&:strip).reject(&:empty?).tally
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
def initialize(counts)
|
|
12
|
+
@counts = counts
|
|
13
|
+
end
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
def hits(file) = @counts.select { |path, _| path.end_with?(file) }.values.max || 0
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
def hot?(members) = changing(members) >= SITES_THAT_DRIFT_APART
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
end
|
|
19
|
+
def changing(members) = members.count { |member| hits(member.file).positive? }
|
|
22
20
|
end
|
data/lib/hashira/ci/accepted.rb
CHANGED
|
@@ -2,47 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Accepted
|
|
8
|
-
Screened = Data.define(:all, :accepted)
|
|
5
|
+
class Hashira::CI::Accepted
|
|
6
|
+
Screened = Data.define(:all, :accepted)
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def matches?(finding) = kind == finding.kind && identity == finding.identity
|
|
8
|
+
Entry =
|
|
9
|
+
Data.define(:kind, :package, :digest, :reason) do
|
|
10
|
+
def self.from(hash)
|
|
11
|
+
new(kind: hash["kind"], package: hash["package"], digest: hash["digest"], reason: hash["reason"])
|
|
12
|
+
end
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
def matches?(finding) = kind == finding.kind && identity == finding.identity
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
def identity = digest || package
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
end
|
|
18
|
+
def label = reason || "accepted (no reason recorded)"
|
|
23
19
|
|
|
24
|
-
def
|
|
25
|
-
|
|
20
|
+
def to_h = { kind:, package:, digest:, reason: }.compact
|
|
21
|
+
end
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
def self.load(path)
|
|
24
|
+
return new([]) unless path && File.exist?(path)
|
|
25
|
+
new(JSON.parse(File.read(path)).fetch("accepted", []))
|
|
26
|
+
end
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
def initialize(entries)
|
|
29
|
+
@entries = entries.map { Entry.from(it) }
|
|
30
|
+
end
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
def entries = @entries.map(&:to_h)
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
def screen(findings)
|
|
35
|
+
accepted, live = findings.map { [it, reason(it)] }.partition(&:last)
|
|
36
|
+
Screened.new(all: live.map(&:first), accepted:)
|
|
37
|
+
end
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
private
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
end
|
|
46
|
-
end
|
|
41
|
+
def reason(finding)
|
|
42
|
+
@entries.find { it.matches?(finding) }&.label
|
|
47
43
|
end
|
|
48
44
|
end
|
data/lib/hashira/ci/baseline.rb
CHANGED
|
@@ -2,39 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Baseline
|
|
8
|
-
SCHEMA_VERSION = 2
|
|
5
|
+
class Hashira::CI::Baseline
|
|
6
|
+
SCHEMA_VERSION = 3
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
def self.load(path) = new(path, File.exist?(path) ? JSON.parse(File.read(path)) : {})
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
def initialize(path, recorded)
|
|
11
|
+
@path = path
|
|
12
|
+
@recorded = recorded
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :path
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
def exist? = File.exist?(@path)
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
def edges = @recorded.fetch("edges", [])
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
def findings = @recorded.fetch("findings", [])
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
def findings? = @recorded.key?("findings")
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
def packaging = @recorded.fetch("packaging", "folder")
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
def write(edges, findings, packaging:)
|
|
28
|
+
File.write(@path, JSON.pretty_generate(payload(edges, findings, packaging)) << "\n")
|
|
29
|
+
end
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
private
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
end
|
|
38
|
-
end
|
|
33
|
+
def payload(edges, findings, packaging)
|
|
34
|
+
base = { version: SCHEMA_VERSION, packaging:, edges:, findings: }
|
|
35
|
+
accepted = Hashira::CI::Accepted.new(@recorded.fetch("accepted", [])).entries
|
|
36
|
+
accepted.empty? ? base : base.merge(accepted:)
|
|
39
37
|
end
|
|
40
38
|
end
|
data/lib/hashira/ci/diff.rb
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Hashira
|
|
4
4
|
module CI
|
|
5
|
-
Diff =
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Diff =
|
|
6
|
+
Data.define(:added, :removed) do
|
|
7
|
+
def self.between(current, recorded)
|
|
8
|
+
new(added: current - recorded, removed: recorded - current)
|
|
9
|
+
end
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
def empty? = added.empty? && removed.empty?
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
def worse? = !added.empty?
|
|
14
|
+
end
|
|
14
15
|
end
|
|
15
16
|
end
|
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@io = io
|
|
9
|
-
end
|
|
3
|
+
class Hashira::CI::EdgeDiffReport
|
|
4
|
+
def initialize(graph, io: $stdout)
|
|
5
|
+
@graph = graph
|
|
6
|
+
@io = io
|
|
7
|
+
end
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
def print(diff)
|
|
10
|
+
diff.added.each { emit(it) }
|
|
11
|
+
Hashira::CI::Improvement.new("Edges removed", io: @io).print(diff.removed)
|
|
12
|
+
end
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
private
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
end
|
|
22
|
-
end
|
|
16
|
+
def emit(edge)
|
|
17
|
+
@io.puts("NEW EDGE #{edge} — introduced by:")
|
|
18
|
+
@graph.evidence(edge.from, edge.to).to_a.sort.each { @io.puts(" · #{it}") }
|
|
23
19
|
end
|
|
24
20
|
end
|
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@io = io
|
|
9
|
-
end
|
|
3
|
+
class Hashira::CI::FindingDiffReport
|
|
4
|
+
def initialize(findings, io: $stdout)
|
|
5
|
+
@findings = findings
|
|
6
|
+
@io = io
|
|
7
|
+
end
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
def print(diff)
|
|
10
|
+
introduced(diff.added).each { emit(it) }
|
|
11
|
+
Hashira::CI::Improvement.new("Findings resolved", io: @io).print(diff.removed)
|
|
12
|
+
end
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
private
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
def introduced(added) = added.filter_map { |signature| @findings.find { it.signature == signature } }
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
end
|
|
24
|
-
end
|
|
18
|
+
def emit(finding)
|
|
19
|
+
@io.puts("NEW FINDING:")
|
|
20
|
+
Hashira::Report::FindingLines.new(finding, indent: " ", io: @io).emit
|
|
25
21
|
end
|
|
26
22
|
end
|
data/lib/hashira/ci/gate.rb
CHANGED
|
@@ -1,31 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@io = io
|
|
10
|
-
end
|
|
3
|
+
class Hashira::CI::Gate
|
|
4
|
+
def initialize(findings, kinds, io: $stdout)
|
|
5
|
+
@findings = findings
|
|
6
|
+
@kinds = kinds
|
|
7
|
+
@io = io
|
|
8
|
+
end
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
def check
|
|
11
|
+
offending = @findings.all.select { @kinds.include?(it.kind) }
|
|
12
|
+
offending.empty? ? clean : failure(offending)
|
|
13
|
+
end
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
private
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
def failure(offending)
|
|
18
|
+
offending.each { Hashira::Report::FindingLines.new(it, io: @io).print }
|
|
19
|
+
@io.puts("\nGate FAILED: #{offending.size} finding(s) of kind #{@kinds.join(", ")}.")
|
|
20
|
+
1
|
|
21
|
+
end
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
end
|
|
29
|
-
end
|
|
23
|
+
def clean
|
|
24
|
+
@io.puts("Gate OK: no findings of kind #{@kinds.join(", ")}.")
|
|
25
|
+
0
|
|
30
26
|
end
|
|
31
27
|
end
|
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@io = io
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def print(removed)
|
|
12
|
-
return if removed.empty?
|
|
3
|
+
class Hashira::CI::Improvement
|
|
4
|
+
def initialize(label, io: $stdout)
|
|
5
|
+
@label = label
|
|
6
|
+
@io = io
|
|
7
|
+
end
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
def print(removed)
|
|
10
|
+
return if removed.empty?
|
|
11
|
+
@io.puts("#{@label} (improvement!): #{removed.join(", ")}")
|
|
12
|
+
@io.puts("Lock it in: hashira --update-baseline")
|
|
18
13
|
end
|
|
19
14
|
end
|
data/lib/hashira/ci/ratchet.rb
CHANGED
|
@@ -1,40 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
class Hashira::CI::Ratchet
|
|
4
|
+
def initialize(graph, findings, baseline_path, io: $stdout)
|
|
5
|
+
@graph = graph
|
|
6
|
+
@findings = findings
|
|
7
|
+
@baseline = Hashira::CI::Baseline.load(baseline_path)
|
|
8
|
+
@io = io
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def update
|
|
12
|
+
@baseline.write(edges, digests, packaging:)
|
|
13
|
+
@io.puts("Baseline updated: #{edges.size} edges, #{digests.size} findings.")
|
|
14
|
+
0
|
|
15
|
+
end
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
def check
|
|
18
|
+
raise(Hashira::Error, "no baseline at #{@baseline.path} — run --update-baseline first") unless @baseline.exist?
|
|
19
|
+
vet
|
|
20
|
+
Hashira::CI::RatchetReport.new(@graph, @findings, io: @io).print(drift, delta)
|
|
21
|
+
end
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
raise Error, "no baseline at #{@baseline.path} — run --update-baseline first" unless @baseline.exist?
|
|
23
|
+
private
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
end
|
|
25
|
+
def packaging = @graph.packaging.to_s
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
def vet
|
|
28
|
+
return if @baseline.packaging == packaging
|
|
29
|
+
raise(Hashira::Error, mismatch)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def mismatch
|
|
33
|
+
recorded = @baseline.packaging
|
|
34
|
+
"baseline #{@baseline.path} was recorded with --package-by #{recorded}, but this run " \
|
|
35
|
+
"uses #{packaging} — rerun with --package-by #{recorded}, or refresh it with --update-baseline"
|
|
36
|
+
end
|
|
26
37
|
|
|
27
|
-
|
|
38
|
+
def edges = @graph.edges.map(&:to_s)
|
|
28
39
|
|
|
29
|
-
|
|
40
|
+
def digests = @findings.map(&:signature).uniq.sort
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
def drift = Hashira::CI::Diff.new(added: fresh, removed: @baseline.edges - edges)
|
|
32
43
|
|
|
33
|
-
|
|
44
|
+
def fresh = @graph.edges.reject { @baseline.edges.include?(it.to_s) }
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
end
|
|
38
|
-
end
|
|
46
|
+
def delta
|
|
47
|
+
Hashira::CI::Diff.between(digests, @baseline.findings) if @baseline.findings?
|
|
39
48
|
end
|
|
40
49
|
end
|
|
@@ -1,42 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@io = io
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def print(edges, findings)
|
|
13
|
-
return unchanged if quiet?(edges, findings)
|
|
14
|
-
|
|
15
|
-
details(edges, findings)
|
|
16
|
-
advice(edges, findings)
|
|
17
|
-
1
|
|
18
|
-
end
|
|
3
|
+
class Hashira::CI::RatchetReport
|
|
4
|
+
def initialize(graph, findings, io: $stdout)
|
|
5
|
+
@graph = graph
|
|
6
|
+
@findings = findings
|
|
7
|
+
@io = io
|
|
8
|
+
end
|
|
19
9
|
|
|
20
|
-
|
|
10
|
+
def print(edges, findings)
|
|
11
|
+
return unchanged if quiet?(edges, findings)
|
|
12
|
+
details(edges, findings)
|
|
13
|
+
advice(edges, findings)
|
|
14
|
+
1
|
|
15
|
+
end
|
|
21
16
|
|
|
22
|
-
|
|
17
|
+
private
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
@io.puts "Ratchet OK: #{@graph.edge_list.size} edges, #{@findings.size} findings, unchanged."
|
|
26
|
-
0
|
|
27
|
-
end
|
|
19
|
+
def quiet?(*diffs) = diffs.compact.all?(&:empty?)
|
|
28
20
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
def unchanged
|
|
22
|
+
@io.puts("Ratchet OK: #{@graph.edges.size} edges, #{@findings.size} findings, unchanged.")
|
|
23
|
+
0
|
|
24
|
+
end
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
def details(edges, findings)
|
|
27
|
+
Hashira::CI::EdgeDiffReport.new(@graph, io: @io).print(edges)
|
|
28
|
+
Hashira::CI::FindingDiffReport.new(@findings, io: @io).print(findings) if findings
|
|
29
|
+
end
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
def advice(*diffs)
|
|
32
|
+
return unless diffs.compact.any?(&:worse?)
|
|
33
|
+
@io.puts("\nRatchet FAILED. Either fix what regressed, or — if it is deliberate —")
|
|
34
|
+
@io.puts("record the decision: update the baseline, or accept it with a reason.")
|
|
41
35
|
end
|
|
42
36
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hashira::CLI::Arguments
|
|
4
|
+
def initialize(argv)
|
|
5
|
+
@argv = argv.dup
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def take(flag, default)
|
|
9
|
+
position = @argv.index(flag)
|
|
10
|
+
return default unless position
|
|
11
|
+
_flag, value = @argv.slice!(position, 2)
|
|
12
|
+
raise(Hashira::Error, "#{flag} needs a value") unless value
|
|
13
|
+
value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def delete(flag) = @argv.delete(flag)
|
|
17
|
+
|
|
18
|
+
def rest
|
|
19
|
+
stray = @argv.find { it.start_with?("-") }
|
|
20
|
+
raise(Hashira::Error, "unknown option #{stray}") if stray
|
|
21
|
+
@argv
|
|
22
|
+
end
|
|
23
|
+
end
|