constable-rails 1.3.1 → 1.3.3
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 +94 -1
- data/README.md +10 -2
- data/lib/constable/cli.rb +32 -11
- data/lib/constable/cold_case/rspec.rb +35 -0
- data/lib/constable/log_router.rb +106 -2
- data/lib/constable/reporter.rb +46 -11
- data/lib/constable/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fb4658a48f23de1dcb97417de5bab1deda4548a334e2a198835f93b7cac656c3
|
|
4
|
+
data.tar.gz: c844e9f7ac1c8d797d60349b7b15759bbcbbb0f1b56c914d43e2511e17f75489
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c3e78d5c2d2ab5314ee501795a92a1017fbfe0eb4cab6889c7ab6883d1ff898b02a595a693c3bb774c8e8e26caed2132c4f0c033723e7389735386f7b6c43668
|
|
7
|
+
data.tar.gz: 9f60ad3894558087d8747a5308e39558ce03d87c1518608dff21c9ca341fc776f11ceea45b7251435f052654ead1bbf348c489086b5857cea8e1b0a845781ff8
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,97 @@ All notable changes to this project are documented here. This project adheres to
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.3.3]
|
|
9
|
+
|
|
10
|
+
**1.3.2 was tagged twice.** The console fix in it was rebuilt after the gem had already
|
|
11
|
+
been pushed, and RubyGems will not accept a second push of the same version — so the
|
|
12
|
+
published 1.3.2 does not contain it. That fix is here, along with two more found on a
|
|
13
|
+
1,277-file suite.
|
|
14
|
+
|
|
15
|
+
### Shared examples survive across cold-case files
|
|
16
|
+
|
|
17
|
+
A suite that keeps shared examples in a plain file beside its specs —
|
|
18
|
+
`require_relative "appeal_shared_examples"` at the top of `appeal_spec.rb` — registers
|
|
19
|
+
them the first time that file loads. `require` never fires again, so every later file
|
|
20
|
+
that shares them died on load:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
✗ spec/models/legacy_appeal_spec.rb "failed to load"
|
|
24
|
+
ArgumentError: Could not find shared examples "toggle overtime"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Twelve files in one run, every one of which passes in isolation. RSpec never hits this
|
|
28
|
+
because it loads every spec file and *then* runs them; Constable loads one at a time,
|
|
29
|
+
which is what makes a cold case cheap, so the registry has to be carried across by hand.
|
|
30
|
+
|
|
31
|
+
Same two files, before and after: 12 files failing to load → `458 passed, 0 failed`.
|
|
32
|
+
|
|
33
|
+
### Rename suggestions are a section, not a wall
|
|
34
|
+
|
|
35
|
+
They were printed unlabelled after SLOWEST, one long line per suggestion, each carrying
|
|
36
|
+
two full test descriptions and two hashes. A real suite produced two hundred of them —
|
|
37
|
+
several hundred lines with no heading, burying every section above.
|
|
38
|
+
|
|
39
|
+
Now a `RENAMED?` section: eight at a time, three short lines each, with the rest counted.
|
|
40
|
+
Descriptions are truncated to the frame, because an RSpec description built from a matcher
|
|
41
|
+
carries the entire inspected object — every column of a record, ids and timestamps
|
|
42
|
+
included.
|
|
43
|
+
|
|
44
|
+
### The app's stdout, again (from 1.3.2, unpublished)
|
|
45
|
+
|
|
46
|
+
Reassigning the `$stdout` object was not enough: a gem writing through the `STDERR`
|
|
47
|
+
constant, or anything already holding the descriptor, goes straight past it, and in a
|
|
48
|
+
terminal both streams land in the same place. The descriptors are now redirected with
|
|
49
|
+
`IO#reopen`, with an `at_exit` guard so a crash still prints where it can be seen.
|
|
50
|
+
|
|
51
|
+
CLI errors no longer use `Kernel#warn`. Rails apps routinely override `Warning.warn` to
|
|
52
|
+
funnel Ruby warnings into `Rails.logger`, and a real one did: `no tests matched ...`
|
|
53
|
+
arrived in `log/test.log` tagged `[RUBY WARNING]` while the terminal showed nothing at
|
|
54
|
+
all, so the command looked like it had silently done nothing.
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## [1.3.2]
|
|
58
|
+
|
|
59
|
+
### stdout is results only — the app's stdout too, not just Constable's
|
|
60
|
+
|
|
61
|
+
`log/test.log` has taken Rails' loggers since 1.0.0. It did not take anything a gem writes
|
|
62
|
+
directly to `$stdout` or `$stderr`, and a warning fired once per file lands in the middle
|
|
63
|
+
of the live stream:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
Address ✓✓✓✓✓✓✓✓✓✓✓To use retry middleware with Faraday v2.0+, install `faraday-retry` gem
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Both streams are now pointed at the log for the duration of a run, and the reporter keeps
|
|
70
|
+
the terminal it captured beforehand. `--verbose` tees them back, which is what that flag
|
|
71
|
+
is for.
|
|
72
|
+
|
|
73
|
+
Two ordering bugs came with it, both caught before release. The console has to be taken
|
|
74
|
+
**before** the Rails check, because `route!` runs before the app boots and most of the
|
|
75
|
+
noise is emitted *by* booting it. And colour detection was asking `$stdout.tty?` — which
|
|
76
|
+
by then is a file, and a file is never a tty, so colour would have silently switched off
|
|
77
|
+
for everybody.
|
|
78
|
+
|
|
79
|
+
Reassigning the `$stdout` *object* turned out not to be enough — a gem writing through
|
|
80
|
+
the `STDERR` constant, or anything already holding the descriptor, goes straight past it,
|
|
81
|
+
and in a terminal both streams land in the same place. So the **descriptors** are
|
|
82
|
+
redirected with `IO#reopen`, which is the only thing that catches every writer, and the
|
|
83
|
+
reporter is handed a dup of the real terminal taken beforehand. An `at_exit` guard puts
|
|
84
|
+
them back, so an uncaught exception still prints its backtrace where you can see it.
|
|
85
|
+
|
|
86
|
+
Two things that fell out of doing it properly:
|
|
87
|
+
|
|
88
|
+
- **CLI errors no longer use `Kernel#warn`.** With the descriptors redirected an error
|
|
89
|
+
would have gone into the log — and `warn` would not have reached the terminal even
|
|
90
|
+
without that, because Rails apps routinely override `Warning.warn` to funnel Ruby
|
|
91
|
+
warnings into `Rails.logger`. A real one did: `no tests matched ...` arrived in
|
|
92
|
+
`log/test.log` tagged `[RUBY WARNING]` while the terminal showed nothing at all. Errors
|
|
93
|
+
now go to the console the reporter kept, on stderr.
|
|
94
|
+
- **Colour detection was asking the wrong stream.** `$stdout.tty?`, when `$stdout` is by
|
|
95
|
+
then a log file and a file is never a tty — colour would have silently switched off for
|
|
96
|
+
everybody.
|
|
97
|
+
|
|
98
|
+
|
|
8
99
|
## [1.3.1]
|
|
9
100
|
|
|
10
101
|
### Cold cases now read `.rspec`
|
|
@@ -528,7 +619,9 @@ Initial release.
|
|
|
528
619
|
- Diff-based coverage gate — only lines changed in the current diff are held to the
|
|
529
620
|
threshold. `constable beat` for the full picture, `--html` for a browsable report.
|
|
530
621
|
|
|
531
|
-
[Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.3.
|
|
622
|
+
[Unreleased]: https://github.com/Ray-Hughes/constable/compare/v1.3.3...HEAD
|
|
623
|
+
[1.3.3]: https://github.com/Ray-Hughes/constable/compare/v1.3.2...v1.3.3
|
|
624
|
+
[1.3.2]: https://github.com/Ray-Hughes/constable/compare/v1.3.1...v1.3.2
|
|
532
625
|
[1.3.1]: https://github.com/Ray-Hughes/constable/compare/v1.3.0...v1.3.1
|
|
533
626
|
[1.3.0]: https://github.com/Ray-Hughes/constable/compare/v1.2.0...v1.3.0
|
|
534
627
|
[1.2.0]: https://github.com/Ray-Hughes/constable/compare/v1.1.0...v1.2.0
|
data/README.md
CHANGED
|
@@ -422,8 +422,16 @@ and says why — slow is a trade-off, wrong is not.
|
|
|
422
422
|
|
|
423
423
|
### Output
|
|
424
424
|
|
|
425
|
-
stdout is reserved for results
|
|
426
|
-
|
|
425
|
+
stdout is reserved for results — not just Constable's own output, but the app's. Rails
|
|
426
|
+
loggers, SQL, request/response logging, and anything a gem prints to `$stdout` or
|
|
427
|
+
`$stderr` mid-run all go to `log/test.log`; `--verbose` streams it back for active
|
|
428
|
+
debugging.
|
|
429
|
+
|
|
430
|
+
That matters more than it sounds. A gem warning fired once per file lands in the middle of
|
|
431
|
+
the live stream, and you get `Address ✓✓✓✓✓✓To use retry middleware with Faraday...`
|
|
432
|
+
instead of a readable run. The one thing Constable deliberately does not intercept is a
|
|
433
|
+
write straight to file descriptor 2 — capturing that would also swallow a real crash and
|
|
434
|
+
break `binding.pry`, so `2>/dev/null` stays yours to decide on.
|
|
427
435
|
|
|
428
436
|
**While it runs**, the live stream has two modes. `concise` is the default: one glyph per
|
|
429
437
|
test, grouped into a run per case, so a thousand-test suite stays inside one screen and a
|
data/lib/constable/cli.rb
CHANGED
|
@@ -21,13 +21,29 @@ module Constable
|
|
|
21
21
|
# tier, an unreadable config. Both are the user's mistake rather than a crash, and
|
|
22
22
|
# both deserve one sentence and a usage status instead of a backtrace.
|
|
23
23
|
rescue Thor::Error, Constable::Error => e
|
|
24
|
-
|
|
24
|
+
complain(e.message)
|
|
25
25
|
exit(EXIT_USAGE)
|
|
26
26
|
rescue Interrupt
|
|
27
|
-
|
|
27
|
+
complain("\ninterrupted")
|
|
28
28
|
exit(EXIT_FAILED)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Anything the user has to see, written where they will see it.
|
|
32
|
+
#
|
|
33
|
+
# Not Kernel#warn. A run points stdout and stderr at log/test.log, so an error has to
|
|
34
|
+
# go to the console the reporter kept -- and `warn` would not reach it even without
|
|
35
|
+
# that: Rails apps routinely override Warning.warn to funnel Ruby warnings into
|
|
36
|
+
# Rails.logger, and a real one did. The message arrived in log/test.log tagged
|
|
37
|
+
# "[RUBY WARNING]" while the terminal showed nothing at all.
|
|
38
|
+
def self.complain(message)
|
|
39
|
+
LogRouter.restore!
|
|
40
|
+
io = LogRouter.console_err
|
|
41
|
+
io.puts(message)
|
|
42
|
+
io.flush if io.respond_to?(:flush)
|
|
43
|
+
rescue StandardError
|
|
44
|
+
$stderr.puts(message) # rubocop:disable Style/StderrPuts
|
|
45
|
+
end
|
|
46
|
+
|
|
31
47
|
class_option :"no-color", type: :boolean, default: false, desc: "Disable ANSI color"
|
|
32
48
|
|
|
33
49
|
desc "test [PATH[:LINE]]", "Run the suite -- native and cold cases side by side"
|
|
@@ -184,7 +200,7 @@ module Constable
|
|
|
184
200
|
option :"show-source", type: :boolean, default: false, desc: "Print the rewritten source"
|
|
185
201
|
def modernize(*paths)
|
|
186
202
|
if paths.empty?
|
|
187
|
-
|
|
203
|
+
CLI.complain("modernize needs at least one path")
|
|
188
204
|
exit(EXIT_USAGE)
|
|
189
205
|
end
|
|
190
206
|
|
|
@@ -309,7 +325,7 @@ module Constable
|
|
|
309
325
|
|
|
310
326
|
identity = jail.resolve(locator)
|
|
311
327
|
unless identity
|
|
312
|
-
|
|
328
|
+
CLI.complain("Nothing on the docket at #{locator}")
|
|
313
329
|
exit(EXIT_USAGE)
|
|
314
330
|
end
|
|
315
331
|
yield jail, identity
|
|
@@ -320,9 +336,9 @@ module Constable
|
|
|
320
336
|
def refuse_ambiguous(locator, candidates, noun)
|
|
321
337
|
return if candidates.size <= 1
|
|
322
338
|
|
|
323
|
-
|
|
339
|
+
CLI.complain("#{locator} matches #{candidates.size} tests #{noun}. Name one:")
|
|
324
340
|
candidates.sort_by { |entry| entry.line.to_i }.each do |entry|
|
|
325
|
-
|
|
341
|
+
CLI.complain(" #{entry.location} #{entry.label}")
|
|
326
342
|
end
|
|
327
343
|
exit(EXIT_USAGE)
|
|
328
344
|
end
|
|
@@ -368,7 +384,7 @@ module Constable
|
|
|
368
384
|
|
|
369
385
|
identity = warrants.resolve(locator)
|
|
370
386
|
unless identity
|
|
371
|
-
|
|
387
|
+
CLI.complain("No warrant at #{locator}")
|
|
372
388
|
exit(EXIT_USAGE)
|
|
373
389
|
end
|
|
374
390
|
warrants.release(identity)
|
|
@@ -413,7 +429,7 @@ module Constable
|
|
|
413
429
|
def prepare
|
|
414
430
|
config = load_config
|
|
415
431
|
unless WorkerDatabases.shardable?
|
|
416
|
-
|
|
432
|
+
CLI.complain("This app has no ActiveRecord test databases to prepare.")
|
|
417
433
|
exit(EXIT_USAGE)
|
|
418
434
|
end
|
|
419
435
|
|
|
@@ -429,7 +445,7 @@ module Constable
|
|
|
429
445
|
"rebuilding them."
|
|
430
446
|
0
|
|
431
447
|
rescue Constable::Error => e
|
|
432
|
-
|
|
448
|
+
CLI.complain(e.message)
|
|
433
449
|
exit(EXIT_FAILED)
|
|
434
450
|
end
|
|
435
451
|
|
|
@@ -501,14 +517,19 @@ module Constable
|
|
|
501
517
|
|
|
502
518
|
def color?
|
|
503
519
|
return false if options[:"no-color"]
|
|
504
|
-
|
|
520
|
+
# The console, not $stdout -- which by now is log/test.log, and a file is never a
|
|
521
|
+
# tty. Asking the wrong one silently turns colour off for everybody.
|
|
522
|
+
return false unless LogRouter.console.tty?
|
|
505
523
|
return false unless ENV["NO_COLOR"].to_s.empty?
|
|
506
524
|
|
|
507
525
|
true
|
|
508
526
|
end
|
|
509
527
|
|
|
528
|
+
# LogRouter.console, not $stdout: by this point route! has pointed $stdout at
|
|
529
|
+
# log/test.log so a stray gem warning cannot land in the middle of the live stream.
|
|
530
|
+
# The reporter is the one thing that still writes to the terminal.
|
|
510
531
|
def reporter(config)
|
|
511
|
-
Reporter.new(io:
|
|
532
|
+
Reporter.new(io: LogRouter.console, config: config, color: color?, mode: output_mode)
|
|
512
533
|
end
|
|
513
534
|
|
|
514
535
|
# nil means "the config file decides". The explicit --output wins over the two
|
|
@@ -149,6 +149,7 @@ module Constable
|
|
|
149
149
|
@session_configuration = nil
|
|
150
150
|
@session_prepared = false
|
|
151
151
|
@ran_before_suite_hooks = nil
|
|
152
|
+
@shared_examples = nil
|
|
152
153
|
nil
|
|
153
154
|
end
|
|
154
155
|
|
|
@@ -252,9 +253,11 @@ module Constable
|
|
|
252
253
|
::RSpec.instance_variable_set(:@configuration, @session_configuration)
|
|
253
254
|
prepare_session_configuration(::RSpec.configuration)
|
|
254
255
|
clear_examples
|
|
256
|
+
restore_shared_examples!
|
|
255
257
|
|
|
256
258
|
yield
|
|
257
259
|
ensure
|
|
260
|
+
remember_shared_examples!
|
|
258
261
|
@session_world = ::RSpec.instance_variable_get(:@world)
|
|
259
262
|
@session_configuration = ::RSpec.instance_variable_get(:@configuration)
|
|
260
263
|
clear_examples
|
|
@@ -325,6 +328,38 @@ module Constable
|
|
|
325
328
|
[]
|
|
326
329
|
end
|
|
327
330
|
|
|
331
|
+
# Shared examples are registered on the world, by `require`, once per process.
|
|
332
|
+
#
|
|
333
|
+
# A suite that keeps them in a plain file next to its specs -- `require_relative
|
|
334
|
+
# "appeal_shared_examples"` at the top of appeal_spec.rb -- registers them while
|
|
335
|
+
# the first file runs. `require` never fires again, so if the registry does not
|
|
336
|
+
# survive into the next file, every later file that shares them dies on load with
|
|
337
|
+
# `Could not find shared examples "toggle overtime"`.
|
|
338
|
+
#
|
|
339
|
+
# Under RSpec that never happens: it loads every spec file first, then runs them.
|
|
340
|
+
# Constable loads one file at a time, which is what makes a cold case cheap, so
|
|
341
|
+
# the registry has to be carried across by hand. Observed on a real suite: twelve
|
|
342
|
+
# files failing to load, all of which pass in isolation.
|
|
343
|
+
def remember_shared_examples!
|
|
344
|
+
world = ::RSpec.instance_variable_get(:@world)
|
|
345
|
+
return unless world.respond_to?(:shared_example_group_registry)
|
|
346
|
+
|
|
347
|
+
@shared_examples = world.shared_example_group_registry
|
|
348
|
+
rescue StandardError
|
|
349
|
+
nil
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def restore_shared_examples!
|
|
353
|
+
return if @shared_examples.nil?
|
|
354
|
+
|
|
355
|
+
world = ::RSpec.world
|
|
356
|
+
return unless world.respond_to?(:shared_example_group_registry)
|
|
357
|
+
|
|
358
|
+
world.instance_variable_set(:@shared_example_group_registry, @shared_examples)
|
|
359
|
+
rescue StandardError
|
|
360
|
+
nil
|
|
361
|
+
end
|
|
362
|
+
|
|
328
363
|
def clear_examples
|
|
329
364
|
world = ::RSpec.instance_variable_get(:@world)
|
|
330
365
|
world.reset if world.respond_to?(:reset)
|
data/lib/constable/log_router.rb
CHANGED
|
@@ -55,6 +55,14 @@ module Constable
|
|
|
55
55
|
def sync = true
|
|
56
56
|
def tty? = false
|
|
57
57
|
|
|
58
|
+
# Something that has been handed $stdout may ask it for a file descriptor. Answer
|
|
59
|
+
# with the first target that has one rather than raising NoMethodError from inside
|
|
60
|
+
# somebody else's gem.
|
|
61
|
+
def fileno
|
|
62
|
+
@targets.each { |t| return t.fileno if t.respond_to?(:fileno) }
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
58
66
|
def sync=(value)
|
|
59
67
|
@targets.each { |target| target.sync = value if target.respond_to?(:sync=) }
|
|
60
68
|
end
|
|
@@ -89,12 +97,18 @@ module Constable
|
|
|
89
97
|
def route!(verbose: false, path: nil, io: $stdout, root: nil)
|
|
90
98
|
target = path ? File.expand_path(path.to_s, root || Constable.root) : default_path(root)
|
|
91
99
|
|
|
100
|
+
# Taking the console does not depend on Rails, and the ordering matters: route! runs
|
|
101
|
+
# before the app boots, so anything that waited for Rails would miss every warning
|
|
102
|
+
# emitted *by* booting it -- which is most of them. A suite with no Rails at all
|
|
103
|
+
# still wants its stdout kept for results.
|
|
104
|
+
file = open_log(target)
|
|
105
|
+
capture_console!(file, verbose ? io : nil)
|
|
106
|
+
|
|
92
107
|
unless rails_loaded?
|
|
93
|
-
reason = "Rails is not loaded —
|
|
108
|
+
reason = "Rails is not loaded — no loggers to route, but stdout is held for results"
|
|
94
109
|
return @current = Routing.new(routed: false, path: target, verbose: verbose, reason: reason)
|
|
95
110
|
end
|
|
96
111
|
|
|
97
|
-
file = open_log(target)
|
|
98
112
|
logger = build_logger(verbose ? Tee.new(file, io) : file)
|
|
99
113
|
|
|
100
114
|
remember_previous!
|
|
@@ -103,9 +117,99 @@ module Constable
|
|
|
103
117
|
@current = Routing.new(routed: true, path: target, verbose: verbose, reason: nil)
|
|
104
118
|
end
|
|
105
119
|
|
|
120
|
+
# The terminal, as it was before route! took it. The reporter writes here; everything
|
|
121
|
+
# else writes to the log.
|
|
122
|
+
def console = @console_out || $stdout
|
|
123
|
+
|
|
124
|
+
# The same, for the stream errors belong on. Convention puts them on stderr, and a
|
|
125
|
+
# run has pointed the real one at the log.
|
|
126
|
+
def console_err = @console_err || $stderr
|
|
127
|
+
|
|
128
|
+
# Rails loggers are not the only thing that writes to a terminal. A gem warning --
|
|
129
|
+
# Faraday's "install the faraday-retry gem", say -- goes straight to $stderr, once
|
|
130
|
+
# per file that triggers it, and lands in the middle of the live stream:
|
|
131
|
+
#
|
|
132
|
+
# Address ✓✓✓✓✓✓✓✓✓✓✓To use retry middleware with Faraday v2.0+...
|
|
133
|
+
#
|
|
134
|
+
# stdout is supposed to be results only. So the app's stdout and stderr are pointed
|
|
135
|
+
# at log/test.log for the duration of the run, and the reporter keeps the real
|
|
136
|
+
# terminal it captured beforehand. `--verbose` tees both back, which is the whole
|
|
137
|
+
# point of that flag.
|
|
138
|
+
def capture_console!(file, tee_to)
|
|
139
|
+
# Reassigning the $stdout *object* is not enough. A gem that writes through the
|
|
140
|
+
# STDERR constant, a C extension, or anything that already holds file descriptor 2
|
|
141
|
+
# goes straight past it -- and in a terminal both streams land in the same place,
|
|
142
|
+
# so "it was only on stderr" is no comfort at all when it lands mid-glyph.
|
|
143
|
+
#
|
|
144
|
+
# So the descriptors themselves are pointed at the log, and the reporter is handed
|
|
145
|
+
# a dup of the real terminal taken beforehand. #reopen changes where fd 1 and 2
|
|
146
|
+
# write for the whole process, which is the only thing that catches every writer.
|
|
147
|
+
@console_out = $stdout.dup
|
|
148
|
+
@console_err = $stderr.dup
|
|
149
|
+
@reopened = false
|
|
150
|
+
|
|
151
|
+
# --verbose means "show me everything", so nothing is redirected; the object swap
|
|
152
|
+
# is enough to tee. The same fallback covers anything that is not a real IO on
|
|
153
|
+
# both sides -- a StringIO standing in for the terminal in a test, most obviously,
|
|
154
|
+
# where IO#reopen has nothing to reopen onto.
|
|
155
|
+
if tee_to || !redirectable?(file)
|
|
156
|
+
@swapped_out = $stdout
|
|
157
|
+
@swapped_err = $stderr
|
|
158
|
+
replacement = tee_to ? Tee.new(file, tee_to) : file
|
|
159
|
+
$stdout = replacement
|
|
160
|
+
$stderr = replacement
|
|
161
|
+
return
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
$stdout.reopen(file)
|
|
165
|
+
$stderr.reopen(file)
|
|
166
|
+
$stdout.sync = true
|
|
167
|
+
$stderr.sync = true
|
|
168
|
+
@reopened = true
|
|
169
|
+
|
|
170
|
+
# Whatever happens next -- a clean exit, a raise, an interrupt -- the descriptors
|
|
171
|
+
# go back. Without this an uncaught exception prints its backtrace into
|
|
172
|
+
# log/test.log and the terminal shows nothing at all, which is a far worse bug
|
|
173
|
+
# than the one being fixed.
|
|
174
|
+
install_exit_guard!
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def install_exit_guard!
|
|
178
|
+
return if @exit_guard_installed
|
|
179
|
+
|
|
180
|
+
@exit_guard_installed = true
|
|
181
|
+
at_exit { restore_console! }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Every party has to be a real IO: the log we are redirecting to, and the two streams
|
|
185
|
+
# we are redirecting away from and will later have to put back.
|
|
186
|
+
def redirectable?(file)
|
|
187
|
+
[file, $stdout, $stderr].all? { |io| io.is_a?(::IO) && io.respond_to?(:fileno) }
|
|
188
|
+
rescue StandardError
|
|
189
|
+
false
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def restore_console!
|
|
193
|
+
if @reopened
|
|
194
|
+
$stdout.reopen(@console_out)
|
|
195
|
+
$stderr.reopen(@console_err)
|
|
196
|
+
elsif @swapped_out
|
|
197
|
+
$stdout = @swapped_out
|
|
198
|
+
$stderr = @swapped_err
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
[@console_out, @console_err].each { |io| io&.close unless io&.closed? } if @reopened
|
|
202
|
+
@console_out = nil
|
|
203
|
+
@console_err = nil
|
|
204
|
+
@swapped_out = nil
|
|
205
|
+
@swapped_err = nil
|
|
206
|
+
@reopened = false
|
|
207
|
+
end
|
|
208
|
+
|
|
106
209
|
# Puts back whatever the app had before route!. Only useful in-process (our own
|
|
107
210
|
# suite, or a REPL); a real run never wants its logs back on stdout.
|
|
108
211
|
def restore!
|
|
212
|
+
restore_console!
|
|
109
213
|
(@previous || {}).each do |constant_name, (setter, logger)|
|
|
110
214
|
constant = resolve(constant_name)
|
|
111
215
|
constant&.public_send(setter, logger)
|
data/lib/constable/reporter.rb
CHANGED
|
@@ -50,6 +50,10 @@ module Constable
|
|
|
50
50
|
# thousand lines that say the same thing.
|
|
51
51
|
COLD_CASE_LIST_LIMIT = 10
|
|
52
52
|
|
|
53
|
+
# How many rename suggestions are listed before the rest are counted instead. A real
|
|
54
|
+
# suite produced two hundred, several hundred lines of output, unlabelled.
|
|
55
|
+
RENAME_LIST_LIMIT = 8
|
|
56
|
+
|
|
53
57
|
DEFAULT_SLOWEST = 5
|
|
54
58
|
|
|
55
59
|
# Column the expanded stream right-aligns durations into. Descriptions are never
|
|
@@ -679,27 +683,58 @@ module Constable
|
|
|
679
683
|
|
|
680
684
|
# Rename detection is the Runner's job; the reporter only says it out loud. Never
|
|
681
685
|
# its own section -- it is a suggestion, not a finding.
|
|
686
|
+
# Renames get a section of their own, a cap, and one line each.
|
|
687
|
+
#
|
|
688
|
+
# They used to be dumped unlabelled after SLOWEST, one long line per suggestion,
|
|
689
|
+
# every line carrying both full test descriptions and two hashes. A real suite
|
|
690
|
+
# produced two hundred of them and several hundred lines of output -- read as a wall
|
|
691
|
+
# of text with no heading, and buried the sections above it. A suggestion nobody can
|
|
692
|
+
# read is not a suggestion.
|
|
682
693
|
def rename_suggestions(suggestions)
|
|
683
|
-
suggestions = Array(suggestions).map { |s|
|
|
694
|
+
suggestions = Array(suggestions).map { |s| suggestion(s) }.compact
|
|
684
695
|
return if suggestions.empty?
|
|
685
696
|
|
|
686
|
-
|
|
687
|
-
suggestions.
|
|
697
|
+
section("RENAMED?")
|
|
698
|
+
shown = suggestions.first(RENAME_LIST_LIMIT)
|
|
699
|
+
|
|
700
|
+
each_entry(shown) do |item|
|
|
701
|
+
writeln(INDENT + paint(item[:from].to_s, :dim))
|
|
702
|
+
writeln(ENTRY_INDENT + paint("→ #{item[:to]}", :dim))
|
|
703
|
+
writeln(ENTRY_INDENT + paint("constable history relink #{item[:old_hash]} #{item[:new_hash]}", :dim))
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
remaining = suggestions.size - shown.size
|
|
707
|
+
hint(if remaining.positive?
|
|
708
|
+
"...and #{remaining} more. A test's history is keyed on its body, so a test " \
|
|
709
|
+
"that was reworded and edited in one commit looks like a new one. Relink the " \
|
|
710
|
+
"ones you recognise; ignore the rest and they age out."
|
|
711
|
+
else
|
|
712
|
+
"A test's history is keyed on its body, so one that was reworded and edited " \
|
|
713
|
+
"in the same commit looks like a new test. Relink it to carry the history over."
|
|
714
|
+
end)
|
|
688
715
|
end
|
|
689
716
|
|
|
690
|
-
def
|
|
691
|
-
return
|
|
692
|
-
return nil unless
|
|
717
|
+
def suggestion(raw)
|
|
718
|
+
return { from: raw.to_s, to: nil, old_hash: nil, new_hash: nil } if raw.is_a?(String)
|
|
719
|
+
return nil unless raw.respond_to?(:to_h)
|
|
693
720
|
|
|
694
|
-
s =
|
|
721
|
+
s = raw.to_h.transform_keys(&:to_sym)
|
|
695
722
|
from = s[:old_label] || s[:from] || label_for(s[:old_case], s[:old_description])
|
|
696
723
|
to = s[:new_label] || s[:to] || label_for(s[:new_case], s[:new_description])
|
|
697
|
-
old_hash = s[:old_hash] || s[:old_identity]
|
|
698
|
-
new_hash = s[:new_hash] || s[:new_identity]
|
|
699
724
|
return nil if from.nil? || to.nil?
|
|
700
725
|
|
|
701
|
-
|
|
702
|
-
|
|
726
|
+
{ from: truncate_label(from), to: truncate_label(to),
|
|
727
|
+
old_hash: s[:old_hash] || s[:old_identity], new_hash: s[:new_hash] || s[:new_identity] }
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
# A description built by RSpec from a matcher carries the whole inspected object --
|
|
731
|
+
# ids, timestamps, every column of a record. Printed in full it is unreadable, and it
|
|
732
|
+
# is the same test either way.
|
|
733
|
+
def truncate_label(label)
|
|
734
|
+
text = label.to_s.tr("\n", " ").squeeze(" ")
|
|
735
|
+
return text if text.length <= RULE_WIDTH - INDENT.length
|
|
736
|
+
|
|
737
|
+
"#{text[0, RULE_WIDTH - INDENT.length - 1]}…"
|
|
703
738
|
end
|
|
704
739
|
|
|
705
740
|
def label_for(case_name, description)
|
data/lib/constable/version.rb
CHANGED