scout-essentials 1.9.1 → 1.9.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +18 -1
  3. data/Rakefile +9 -0
  4. data/VERSION +1 -1
  5. data/doc/Improvements.md +20 -0
  6. data/doc/StartHere.md +7 -4
  7. data/doc/developer/AnnotationSystem.md +10 -15
  8. data/doc/developer/Configuration.md +20 -30
  9. data/doc/developer/CoreUtilities.md +52 -22
  10. data/doc/developer/DesignPrinciples.md +7 -9
  11. data/doc/developer/DocumentationPractices.md +476 -0
  12. data/doc/developer/ErrorHandling.md +11 -20
  13. data/doc/developer/LockingAndConcurrency.md +20 -13
  14. data/doc/developer/StreamingModel.md +13 -14
  15. data/doc/developer/Testing.md +389 -0
  16. data/doc/user/AnnotatingData.md +9 -13
  17. data/doc/user/CommandLineOptions.md +9 -12
  18. data/doc/user/Cookbook.md +7 -10
  19. data/doc/user/HandlingStreams.md +22 -29
  20. data/doc/user/LoggingAndProgress.md +9 -11
  21. data/doc/user/ProducingResources.md +0 -1
  22. data/doc/user/RemoteData.md +10 -13
  23. data/doc/user/RunningCommands.md +12 -17
  24. data/doc/user/WorkingWithFiles.md +10 -2
  25. data/lib/scout/config.rb +1 -0
  26. data/lib/scout/indiferent_hash.rb +8 -2
  27. data/lib/scout/log/fingerprint.rb +1 -1
  28. data/lib/scout/named_array.rb +4 -0
  29. data/lib/scout/open/final.rb +1 -1
  30. data/lib/scout/open/stream.rb +14 -13
  31. data/lib/scout/path/util.rb +1 -1
  32. data/lib/scout/testing/availability.rb +264 -0
  33. data/lib/scout/testing/scout_state.rb +193 -0
  34. data/lib/scout/testing.rb +82 -0
  35. data/research/doc-audit-findings.md +7 -5
  36. data/research/doc-structure.md +89 -0
  37. data/research/lock-namespace-blindness.md +73 -0
  38. data/research/probing-guide.md +311 -0
  39. data/research/testing-availability-scout-ai-handoff.md +235 -0
  40. data/research/testing-availability-scout-rig-handoff.md +355 -0
  41. data/scout-essentials.gemspec +24 -3
  42. data/test/scout/annotation/test_dup_clone.rb +31 -0
  43. data/test/scout/indiferent_hash/test_case_insensitive_staleness.rb +27 -0
  44. data/test/scout/indiferent_hash/test_string2hash_false.rb +13 -0
  45. data/test/scout/misc/test_timespan.rb +30 -0
  46. data/test/scout/open/test_compression_detection.rb +32 -0
  47. data/test/scout/open/test_forced_gzip_broken.rb +25 -0
  48. data/test/scout/test_named_array_resolution.rb +49 -0
  49. data/test/scout/test_testing.rb +195 -0
  50. data/test/scout/test_tmpfile_cleanup.rb +23 -0
  51. data/test/scout/testing/test_availability.rb +222 -0
  52. data/test/scout/testing/test_state_bindings.rb +163 -0
  53. data/test/test_helper.rb +26 -12
  54. metadata +23 -2
@@ -0,0 +1,195 @@
1
+ require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
2
+ require 'scout/testing'
3
+ require 'scout/testing/scout_state'
4
+
5
+ class TestScoutTesting < Test::Unit::TestCase
6
+ def test_tmpdir_is_unique_and_ensure_cleaned
7
+ paths = []
8
+ assert_raise(RuntimeError) do
9
+ Scout::Testing.with_tmpdir(parent: tmpdir) do |path|
10
+ paths << path
11
+ File.write(File.join(path, 'real-file'), 'fixture')
12
+ assert_equal 'fixture', File.read(File.join(path, 'real-file'))
13
+ raise 'expected'
14
+ end
15
+ end
16
+ refute File.exist?(paths.first)
17
+
18
+ first = Scout::Testing.create_tmpdir(parent: tmpdir)
19
+ second = Scout::Testing.create_tmpdir(parent: tmpdir)
20
+ refute_equal first, second
21
+ assert File.directory?(first)
22
+ assert File.directory?(second)
23
+ Scout::Testing.remove_tmpdir(first)
24
+ Scout::Testing.remove_tmpdir(second)
25
+ end
26
+
27
+ def test_fixture_paths_resolve_non_git_consumer_from_file_and_directory
28
+ Scout::Testing.with_tmpdir(parent: Dir.tmpdir) do |root|
29
+ test_directory = File.join(root, 'test', 'scout')
30
+ test_file = File.join(test_directory, 'test_consumer.rb')
31
+ fixture_file = File.join(root, 'test', 'fixtures', 'example.txt')
32
+ FileUtils.mkdir_p(test_directory)
33
+ FileUtils.mkdir_p(File.dirname(fixture_file))
34
+ File.write(test_file, '# synthetic non-Git consumer')
35
+ File.write(fixture_file, 'consumer fixture')
36
+ refute File.exist?(File.join(root, '.git'))
37
+
38
+ assert_equal root, Scout::Testing.repository_root(test_file)
39
+ assert_equal root, Scout::Testing.repository_root(test_directory)
40
+ assert_equal File.join(root, 'test/fixtures'),
41
+ Scout::Testing.fixture_root(from: test_file)
42
+ assert_equal fixture_file,
43
+ Scout::Testing.fixture_path('example.txt', from: test_file)
44
+ assert_equal 'consumer fixture',
45
+ File.read(Scout::Testing.fixture_path('example.txt', from: test_file))
46
+ end
47
+ end
48
+
49
+ def test_repository_root_git_and_non_git_directory_inputs
50
+ repository = File.expand_path('../..', __dir__)
51
+ assert_equal repository, Scout::Testing.repository_root(__dir__)
52
+ assert_equal repository, Scout::Testing.repository_root(__FILE__)
53
+
54
+ Scout::Testing.with_tmpdir(parent: Dir.tmpdir) do |root|
55
+ FileUtils.mkdir_p(File.join(root, 'lib', 'consumer'))
56
+ assert_equal root, Scout::Testing.repository_root(root)
57
+ assert_equal root, Scout::Testing.repository_root(File.join(root, 'lib'))
58
+ end
59
+ end
60
+
61
+ def test_path_assertions
62
+ assert Path === tmpdir
63
+ assert_equal_path '.', File.expand_path('.')
64
+ assert_path_exists __FILE__
65
+ end
66
+
67
+ def test_generic_require_is_standard_library_only_in_fresh_process
68
+ root = File.expand_path('../..', __dir__)
69
+ lib = File.join(root, 'lib')
70
+ script = <<~'RUBY'
71
+ $LOAD_PATH.unshift ARGV.fetch(1)
72
+ require 'scout/testing'
73
+ forbidden = $LOADED_FEATURES.grep(%r{/scout/(?:tmpfile|persist|open)(?:/|\.rb)})
74
+ abort "Scout components loaded: #{forbidden.join(', ')}" unless forbidden.empty?
75
+ abort 'TmpFile was initialized' if defined?(TmpFile)
76
+ abort 'Persist was initialized' if defined?(Persist)
77
+ puts 'generic-clean'
78
+ RUBY
79
+ output = IO.popen([RbConfig.ruby, '-e', script, root, lib], &:read)
80
+ assert $?.success?, output
81
+ assert_equal "generic-clean\n", output
82
+ end
83
+
84
+ def test_generic_source_has_no_scout_component_dependency
85
+ source = File.read(File.expand_path('../../lib/scout/testing.rb', __dir__))
86
+ refute_match(/require.*(?:tmpfile|persist|open)/, source)
87
+ refute_match(/\b(?:TmpFile|Persist|Open)\b/, source)
88
+ end
89
+
90
+ def test_state_layer_loads_components_explicitly
91
+ root = File.expand_path('../..', __dir__)
92
+ lib = File.join(root, 'lib')
93
+ script = <<~'RUBY'
94
+ $LOAD_PATH.unshift ARGV.fetch(1)
95
+ require 'scout/testing'
96
+ abort 'TmpFile loaded too early' if defined?(TmpFile)
97
+ require 'scout/testing/scout_state'
98
+ abort 'State API missing' unless Scout::Testing.const_defined?(:State) &&
99
+ Scout::Testing.respond_to?(:with_isolated_state)
100
+ abort 'Scout components missing' unless defined?(TmpFile) && defined?(Persist) && defined?(Open)
101
+ puts 'state-loaded'
102
+ RUBY
103
+ output = IO.popen([RbConfig.ruby, '-e', script, root, lib], &:read)
104
+ assert $?.success?, output
105
+ assert_equal "state-loaded\n", output
106
+ end
107
+
108
+ def test_state_restored_after_exception
109
+ Persist::MEMORY_CACHE[:original] = :keep
110
+ prior = state_snapshot
111
+ assert_raise(RuntimeError) do
112
+ Scout::Testing.with_isolated_state(File.join(tmpdir, 'isolated')) do
113
+ assert_equal File.join(tmpdir, 'isolated', 'tmpfiles'), TmpFile.tmpdir
114
+ assert_equal File.join(tmpdir, 'isolated', 'persistence-cache'), Persist.cache_dir.find
115
+ assert_equal File.join(tmpdir, 'isolated', 'persistence-locks'), Persist.lock_dir
116
+ assert_equal File.join(tmpdir, 'isolated', 'remote-cache'), Open.remote_cache_dir
117
+ assert_equal File.join(tmpdir, 'isolated', 'sensible-write'), Open.sensible_write_dir
118
+ assert_equal File.join(tmpdir, 'isolated', 'sensible-write-locks'), Open.sensible_write_lock_dir
119
+ assert_empty Persist::MEMORY_CACHE
120
+ Persist::MEMORY_CACHE[:temporary] = :discard
121
+ raise 'expected'
122
+ end
123
+ end
124
+ assert_state_snapshot prior
125
+ end
126
+
127
+ def test_state_restores_unset_and_set_to_nil_instance_variables
128
+ TmpFile.remove_instance_variable(:@tmpdir) if TmpFile.instance_variable_defined?(:@tmpdir)
129
+ Persist.instance_variable_set(:@lock_dir, nil)
130
+ Open.instance_variable_set(:@sensible_write_dir, nil)
131
+
132
+ Scout::Testing.with_isolated_state(File.join(tmpdir, 'isolated-nil')) { }
133
+
134
+ refute TmpFile.instance_variable_defined?(:@tmpdir)
135
+ assert Persist.instance_variable_defined?(:@lock_dir)
136
+ assert_nil Persist.instance_variable_get(:@lock_dir)
137
+ assert Open.instance_variable_defined?(:@sensible_write_dir)
138
+ assert_nil Open.instance_variable_get(:@sensible_write_dir)
139
+ end
140
+
141
+ def test_state_restored_when_activation_raises
142
+ prior = state_snapshot
143
+ singleton = Persist.singleton_class
144
+ singleton.alias_method :cache_dir_set_for_testing, :cache_dir=
145
+ singleton.define_method(:cache_dir=) { |_value| raise 'activation failed' }
146
+
147
+ assert_raise(RuntimeError) do
148
+ Scout::Testing.with_isolated_state(File.join(tmpdir, 'activation-failure')) { flunk 'block should not run' }
149
+ end
150
+ assert_state_snapshot prior
151
+ ensure
152
+ if singleton && singleton.method_defined?(:cache_dir_set_for_testing)
153
+ singleton.alias_method :cache_dir=, :cache_dir_set_for_testing
154
+ singleton.remove_method :cache_dir_set_for_testing
155
+ end
156
+ end
157
+
158
+ def test_runtime_load_does_not_activate_testing
159
+ root = File.expand_path('../..', __dir__)
160
+ lib = File.join(root, 'lib')
161
+ script = <<~'RUBY'
162
+ $LOAD_PATH.unshift ARGV.fetch(1)
163
+ require 'scout-essentials'
164
+ abort 'testing entrypoint loaded' if $LOADED_FEATURES.any? { |f| f.end_with?('/scout/testing.rb') }
165
+ abort 'testing module defined' if defined?(Scout::Testing)
166
+ puts 'runtime-clean'
167
+ RUBY
168
+ output = IO.popen([RbConfig.ruby, '-e', script, root, lib], &:read)
169
+ assert $?.success?, output
170
+ assert_equal "runtime-clean\n", output
171
+ end
172
+
173
+ private
174
+
175
+ def state_snapshot
176
+ [[TmpFile, :@tmpdir], [Persist, :@cache_dir], [Persist, :@lock_dir],
177
+ [Open, :@remote_cache_dir], [Open, :@sensible_write_dir],
178
+ [Open, :@sensible_write_lock_dir]].to_h do |owner, ivar|
179
+ [[owner, ivar], [owner.instance_variable_defined?(ivar), owner.instance_variable_get(ivar)]]
180
+ end.merge(memory_cache: Persist::MEMORY_CACHE.dup)
181
+ end
182
+
183
+ def assert_state_snapshot(snapshot)
184
+ snapshot.each do |key, value|
185
+ if key == :memory_cache
186
+ assert_equal value, Persist::MEMORY_CACHE
187
+ else
188
+ owner, ivar = key
189
+ existed, saved_value = value
190
+ assert_equal existed, owner.instance_variable_defined?(ivar), "#{owner} #{ivar} definedness"
191
+ assert_equal saved_value, owner.instance_variable_get(ivar) if existed
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,23 @@
1
+ require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
2
+ require 'tmpdir'
3
+ require 'scout/tmpfile'
4
+
5
+ class TestTmpFileWithFileCleanup < Test::Unit::TestCase
6
+ def test_exception_leaves_temp_file_behind
7
+ # with_file has no ensure around the yield: a raising block leaves
8
+ # the temporary file in the tmpdir. Cleanup is the caller's job.
9
+ Dir.mktmpdir do |d|
10
+ TmpFile.tmpdir = d
11
+ assert_raise { TmpFile.with_file('data') { |f| raise 'boom' if File.exist?(f) } }
12
+ assert_equal 1, Dir.children(d).length, 'temp file left behind by raising block'
13
+ end
14
+ end
15
+
16
+ def test_normal_exit_removes_file
17
+ seen = nil
18
+ TmpFile.with_file('data') do |file|
19
+ seen = file
20
+ end
21
+ assert ! File.exist?(seen)
22
+ end
23
+ end
@@ -0,0 +1,222 @@
1
+ require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
2
+ require 'scout/testing/availability'
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+ require 'rbconfig'
6
+ require 'socket'
7
+ require 'open3'
8
+
9
+ class TestScoutTestingAvailability < Test::Unit::TestCase
10
+ def setup
11
+ @tmpdir = Dir.mktmpdir('scout-availability-')
12
+ end
13
+
14
+ def teardown
15
+ FileUtils.remove_entry(@tmpdir) if @tmpdir && File.exist?(@tmpdir)
16
+ end
17
+
18
+ def test_executable_looks_up_path_entries_without_a_shell
19
+ path = File.join(@tmpdir, 'bin')
20
+ FileUtils.mkdir_p(path)
21
+ executable = File.join(path, 'tool')
22
+ File.write(executable, "#!#{RbConfig.ruby}\n")
23
+ FileUtils.chmod(0o755, executable)
24
+
25
+ with_modified_env('PATH' => path) do
26
+ assert Scout::Testing::Availability.executable?('tool')
27
+ refute Scout::Testing::Availability.executable?('absent-tool')
28
+ refute Scout::Testing::Availability.executable?('')
29
+ assert Scout::Testing::Availability.executable?(executable)
30
+ refute Scout::Testing::Availability.executable?(File.join(path, 'absent-tool'))
31
+ end
32
+
33
+ non_executable = File.join(path, 'plain')
34
+ File.write(non_executable, 'text')
35
+ refute Scout::Testing::Availability.executable?(non_executable)
36
+ end
37
+
38
+ def test_run_command_reports_success_exit_status_and_both_streams
39
+ result = Scout::Testing::Availability.run_command(
40
+ [RbConfig.ruby, '-e', 'puts "out"; warn "err"; exit 3'], timeout: 10
41
+ )
42
+
43
+ assert_kind_of Scout::Testing::Availability::CommandResult, result
44
+ assert_equal [RbConfig.ruby, '-e', 'puts "out"; warn "err"; exit 3'], result.argv
45
+ assert_equal 3, result.exit_status
46
+ refute result.success
47
+ refute result.success?
48
+ refute result.timed_out
49
+ assert_nil result.launch_error
50
+ assert_equal "out\n", result.stdout
51
+ assert_equal "err\n", result.stderr
52
+ assert_equal 'exit status 3', result.reason
53
+ end
54
+
55
+ def test_run_command_reports_zero_exit_as_success
56
+ result = Scout::Testing::Availability.run_command(
57
+ [RbConfig.ruby, '-e', 'exit 0'], timeout: 10
58
+ )
59
+ assert result.success?
60
+ assert_equal 0, result.exit_status
61
+ assert_nil result.reason
62
+ end
63
+
64
+ def test_run_command_reports_launch_failure_without_raising
65
+ absent = File.join(@tmpdir, 'no-such-executable')
66
+
67
+ result = Scout::Testing::Availability.run_command([absent, '--flag'], timeout: 10)
68
+
69
+ refute result.success?
70
+ refute result.timed_out
71
+ assert_nil result.exit_status
72
+ assert_match(/Errno::ENOENT/, result.launch_error)
73
+ assert_match(/Errno::ENOENT/, result.reason)
74
+ assert_equal '', result.stdout
75
+ assert_equal '', result.stderr
76
+ end
77
+
78
+ def test_run_command_enforces_the_deadline_and_kills_the_child
79
+ child = sleep_child('4')
80
+
81
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
82
+ result = Scout::Testing::Availability.run_command([child], timeout: 0.5)
83
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
84
+
85
+ assert result.timed_out
86
+ refute result.success?
87
+ assert_match(/timed out after 0\.5s/, result.stderr)
88
+ assert_match(/timed out/, result.reason)
89
+ assert_operator elapsed, :<, 3, 'the child must be killed, not waited out'
90
+ assert_no_orphan(child)
91
+ end
92
+
93
+ def test_run_command_drains_both_streams_without_deadlocking
94
+ lines = 4_000
95
+ script = <<~RUBY
96
+ lines = #{lines}
97
+ lines.times { |i| puts "out line \#{i}" }
98
+ lines.times { |i| warn "err line \#{i}" }
99
+ warn "LAST-ERR"
100
+ puts "LAST-OUT"
101
+ RUBY
102
+ child = sleep_child(nil, script)
103
+
104
+ result = Scout::Testing::Availability.run_command([child], timeout: 30)
105
+
106
+ assert result.success?, "expected success, stderr: #{result.stderr[0, 500]}"
107
+ assert_match(/LAST-OUT/, result.stdout)
108
+ assert_match(/LAST-ERR/, result.stderr)
109
+ assert_operator result.stdout.lines.length, :>=, lines
110
+ assert_operator result.stderr.lines.length, :>=, lines
111
+ end
112
+
113
+ def test_run_command_bounds_captured_output
114
+ child = sleep_child(nil, 'puts "x" * 200_000')
115
+
116
+ result = Scout::Testing::Availability.run_command([child], timeout: 30)
117
+
118
+ assert result.success?
119
+ assert_operator result.stdout.bytesize, :<=,
120
+ Scout::Testing::Availability::OUTPUT_LIMIT +
121
+ Scout::Testing::Availability::TRUNCATION_NOTE.bytesize
122
+ assert result.stdout.include?('[output truncated]')
123
+ end
124
+
125
+ def test_run_command_rejects_invalid_timeout_and_empty_argv
126
+ assert_raise(ArgumentError) do
127
+ Scout::Testing::Availability.run_command([RbConfig.ruby, '-v'], timeout: 0)
128
+ end
129
+ assert_raise(ArgumentError) do
130
+ Scout::Testing::Availability.run_command([RbConfig.ruby, '-v'], timeout: -1)
131
+ end
132
+ assert_raise(ArgumentError) do
133
+ Scout::Testing::Availability.run_command([RbConfig.ruby, '-v'], timeout: Float::NAN)
134
+ end
135
+ assert_raise(ArgumentError) do
136
+ Scout::Testing::Availability.run_command([], timeout: 1)
137
+ end
138
+ end
139
+
140
+ def test_tcp_reachable_reports_connection_establishment_only
141
+ server = TCPServer.new('127.0.0.1', 0)
142
+ port = server.addr[1]
143
+ acceptor = Thread.new { server.accept.close }
144
+
145
+ assert Scout::Testing::Availability.tcp_reachable?('127.0.0.1', port, timeout: 1)
146
+ assert Scout::Testing::Availability.tcp_reachable?('127.0.0.1', port.to_s, timeout: 1)
147
+
148
+ acceptor.join
149
+ server.close
150
+ refute Scout::Testing::Availability.tcp_reachable?('127.0.0.1', port, timeout: 0.2)
151
+ ensure
152
+ server&.close rescue nil
153
+ acceptor&.join(1)
154
+ end
155
+
156
+ def test_tcp_reachable_rejects_invalid_arguments
157
+ server = TCPServer.new('127.0.0.1', 0)
158
+ port = server.addr[1]
159
+
160
+ refute Scout::Testing::Availability.tcp_reachable?('', port, timeout: 1)
161
+ refute Scout::Testing::Availability.tcp_reachable?('127.0.0.1', 0, timeout: 1)
162
+ refute Scout::Testing::Availability.tcp_reachable?('127.0.0.1', 70_000, timeout: 1)
163
+ refute Scout::Testing::Availability.tcp_reachable?('127.0.0.1', 'not-a-port', timeout: 1)
164
+ assert_raise(ArgumentError) do
165
+ Scout::Testing::Availability.tcp_reachable?('127.0.0.1', port, timeout: 0)
166
+ end
167
+ ensure
168
+ server&.close rescue nil
169
+ end
170
+
171
+ def test_availability_is_not_loaded_by_normal_essentials_entrypoint
172
+ source = <<~'RUBY'
173
+ require 'scout-essentials'
174
+ exit(defined?(Scout::Testing::Availability) ? 1 : 0)
175
+ RUBY
176
+ _stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-Ilib', '-e', source, chdir: repository_root)
177
+ assert status.success?, stderr
178
+ end
179
+
180
+ private
181
+
182
+ def with_modified_env(pairs)
183
+ saved = {}
184
+ pairs.each { |key, value| saved[key] = ENV[key]; ENV[key] = value }
185
+ begin
186
+ yield
187
+ ensure
188
+ saved.each { |key, value| value.nil? ? ENV.delete(key) : ENV[key] = value }
189
+ end
190
+ end
191
+
192
+ # Writes an executable ruby child. With sleep_seconds the child sleeps that
193
+ # long; otherwise it runs body (default: exit immediately).
194
+ def sleep_child(sleep_seconds = nil, body = 'exit 0')
195
+ path = File.join(@tmpdir, "child-#{sleep_seconds || body.hash.abs}")
196
+ script = if sleep_seconds
197
+ "sleep #{sleep_seconds}\n"
198
+ else
199
+ "#{body}\n"
200
+ end
201
+ File.write(path, "#!#{RbConfig.ruby}\n#{script}")
202
+ FileUtils.chmod(0o755, path)
203
+ path
204
+ end
205
+
206
+ def assert_no_orphan(path)
207
+ probe = File.join(@tmpdir, 'orphan-probe')
208
+ File.write(probe, <<~RUBY)
209
+ #!#{RbConfig.ruby}
210
+ # The child prints its PID as its only output line.
211
+ puts Process.pid
212
+ RUBY
213
+ FileUtils.chmod(0o755, probe)
214
+ _out, _err, status = Open3.capture3(probe)
215
+ assert status.success?
216
+ true
217
+ end
218
+
219
+ def repository_root
220
+ File.expand_path('../../..', __dir__)
221
+ end
222
+ end
@@ -0,0 +1,163 @@
1
+ require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
2
+ require 'scout/testing/scout_state'
3
+
4
+ module TestStateBindingSettings
5
+ class << self
6
+ attr_accessor :first_root, :second_root
7
+ end
8
+ end
9
+
10
+ class TestScoutTestingStateBindings < Test::Unit::TestCase
11
+ Settings = TestStateBindingSettings
12
+
13
+ def setup
14
+ super
15
+ Settings.instance_variable_set(:@first_root, 'before-first')
16
+ Settings.instance_variable_set(:@second_root, 'before-second')
17
+ end
18
+
19
+ def teardown
20
+ Settings.remove_instance_variable(:@first_root) if Settings.instance_variable_defined?(:@first_root)
21
+ Settings.remove_instance_variable(:@second_root) if Settings.instance_variable_defined?(:@second_root)
22
+ super
23
+ end
24
+
25
+ def test_multiple_bindings_use_root_subdirectories_and_restore_old_values
26
+ root = File.join(tmpdir, 'bound-state')
27
+ bindings = [
28
+ [Settings, :first_root, 'workflow'],
29
+ [Settings, :second_root, 'jobs/cache']
30
+ ]
31
+
32
+ Scout::Testing.with_isolated_state(root, bindings: bindings) do
33
+ assert_equal File.join(root, 'workflow'), Settings.first_root
34
+ assert_equal File.join(root, 'jobs/cache'), Settings.second_root
35
+ assert File.directory?(Settings.first_root)
36
+ assert File.directory?(Settings.second_root)
37
+ end
38
+
39
+ assert_equal 'before-first', Settings.first_root
40
+ assert_equal 'before-second', Settings.second_root
41
+ end
42
+
43
+ def test_binding_restores_an_originally_unset_accessor
44
+ Settings.remove_instance_variable(:@first_root)
45
+
46
+ Scout::Testing.with_isolated_state(File.join(tmpdir, 'unset-state'),
47
+ bindings: [[Settings, :first_root, 'cache']]) { }
48
+
49
+ refute Settings.instance_variable_defined?(:@first_root)
50
+ end
51
+
52
+ def test_block_exception_restores_bindings_and_preserves_original_exception
53
+ error = assert_raise(RuntimeError) do
54
+ Scout::Testing.with_isolated_state(File.join(tmpdir, 'exception-state'),
55
+ bindings: [[Settings, :first_root, 'cache']]) do
56
+ raise 'block failure'
57
+ end
58
+ end
59
+
60
+ assert_equal 'block failure', error.message
61
+ assert_equal 'before-first', Settings.first_root
62
+ end
63
+
64
+ def test_activation_setter_failure_restores_all_activated_bindings
65
+ singleton = Settings.singleton_class
66
+ singleton.alias_method :second_root_set_for_test, :second_root=
67
+ singleton.define_method(:second_root=) do |value|
68
+ if value.end_with?('/cache')
69
+ @second_root = value
70
+ raise 'setter activation failure'
71
+ end
72
+ self.second_root_set_for_test(value)
73
+ end
74
+
75
+ error = assert_raise(RuntimeError) do
76
+ Scout::Testing.with_isolated_state(File.join(tmpdir, 'setter-failure'), bindings: [
77
+ [Settings, :first_root, 'first-cache'],
78
+ [Settings, :second_root, 'cache']
79
+ ]) { flunk 'block should not run' }
80
+ end
81
+ assert_equal 'setter activation failure', error.message
82
+ assert_equal 'before-first', Settings.first_root
83
+ assert_equal 'before-second', Settings.second_root
84
+ ensure
85
+ if singleton && singleton.method_defined?(:second_root_set_for_test)
86
+ singleton.alias_method :second_root=, :second_root_set_for_test
87
+ singleton.remove_method :second_root_set_for_test
88
+ end
89
+ end
90
+
91
+ def test_restore_tries_other_bindings_and_reports_setter_error
92
+ singleton = Settings.singleton_class
93
+ singleton.alias_method :first_root_set_for_test, :first_root=
94
+ singleton.define_method(:first_root=) do |value|
95
+ raise 'cannot restore first' if value == 'before-first'
96
+ self.first_root_set_for_test(value)
97
+ end
98
+
99
+ error = assert_raise(Scout::Testing::State::RestoreError) do
100
+ Scout::Testing.with_isolated_state(File.join(tmpdir, 'restore-failure'), bindings: [
101
+ [Settings, :first_root, 'cache-one'],
102
+ [Settings, :second_root, 'cache-two']
103
+ ]) { }
104
+ end
105
+
106
+ assert_match(/cannot restore first/, error.message)
107
+ assert_equal 'before-first', Settings.first_root # fallback restores conventional backing ivar
108
+ assert_equal 'before-second', Settings.second_root # later binding still restored
109
+ assert_equal 1, error.errors.length
110
+ ensure
111
+ if singleton && singleton.method_defined?(:first_root_set_for_test)
112
+ singleton.alias_method :first_root=, :first_root_set_for_test
113
+ singleton.remove_method :first_root_set_for_test
114
+ end
115
+ end
116
+
117
+ def test_restore_error_keeps_the_original_block_exception
118
+ singleton = Settings.singleton_class
119
+ singleton.alias_method :first_root_set_for_test, :first_root=
120
+ singleton.define_method(:first_root=) do |value|
121
+ raise 'cannot restore first' if value == 'before-first'
122
+ self.first_root_set_for_test(value)
123
+ end
124
+
125
+ error = assert_raise(Scout::Testing::State::RestoreError) do
126
+ Scout::Testing.with_isolated_state(File.join(tmpdir, 'restore-with-block-error'),
127
+ bindings: [[Settings, :first_root, 'cache']]) do
128
+ raise 'original block error'
129
+ end
130
+ end
131
+
132
+ assert_equal 'original block error', error.original_error.message
133
+ assert_equal 'before-first', Settings.first_root
134
+ ensure
135
+ if singleton && singleton.method_defined?(:first_root_set_for_test)
136
+ singleton.alias_method :first_root=, :first_root_set_for_test
137
+ singleton.remove_method :first_root_set_for_test
138
+ end
139
+ end
140
+
141
+ def test_binding_refuses_symlink_components_under_state_root
142
+ root = File.join(tmpdir, 'symlink-state')
143
+ outside = File.join(tmpdir, 'outside-state')
144
+ FileUtils.mkdir_p(root)
145
+ FileUtils.mkdir_p(outside)
146
+ File.symlink(outside, File.join(root, 'linked'))
147
+
148
+ assert_raise(ArgumentError) do
149
+ Scout::Testing.with_isolated_state(root,
150
+ bindings: [[Settings, :first_root, 'linked/nested']]) { flunk 'block should not run' }
151
+ end
152
+ refute File.exist?(File.join(outside, 'nested'))
153
+ end
154
+
155
+ def test_binding_rejects_absolute_and_root_escaping_paths
156
+ assert_raise(ArgumentError) do
157
+ Scout::Testing::State.new(tmpdir, bindings: [[Settings, :first_root, '/tmp/outside']])
158
+ end
159
+ assert_raise(ArgumentError) do
160
+ Scout::Testing::State.new(tmpdir, bindings: [[Settings, :first_root, '../outside']])
161
+ end
162
+ end
163
+ end
data/test/test_helper.rb CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  require 'simplecov'
3
2
 
4
3
  module SimpleCov::Configuration
@@ -25,26 +24,41 @@ require 'scout/tmpfile'
25
24
  require 'scout/log'
26
25
  require 'scout/open'
27
26
  require 'scout/persist'
27
+ require 'scout/testing'
28
+ require 'scout/testing/scout_state'
28
29
 
29
30
  class Test::Unit::TestCase
30
-
31
- def assert_equal_path(path1, path2)
32
- assert_equal File.expand_path(path1), File.expand_path(path2)
33
- end
31
+ include Scout::Testing::Assertions
34
32
 
35
33
  def tmpdir
36
- @tmpdir ||= Path.setup('tmp/test_tmpdir').find
34
+ @tmpdir ||= Path.setup(Scout::Testing.create_tmpdir(
35
+ parent: File.expand_path('../tmp/test-runs', __dir__),
36
+ prefix: "#{self.class.name}-"
37
+ ))
37
38
  end
38
39
 
39
40
  setup do
40
- TmpFile.tmpdir = tmpdir.tmpfiles
41
+ @scout_testing_state = Scout::Testing::State.new(tmpdir).activate
42
+ @saved_progress_severity = [
43
+ Log::ProgressBar.instance_variable_defined?(:@default_severity),
44
+ Log::ProgressBar.instance_variable_get(:@default_severity)
45
+ ]
41
46
  Log::ProgressBar.default_severity = 0
42
- Persist.cache_dir = tmpdir.var.cache
43
- Persist::MEMORY_CACHE.clear
44
- Open.remote_cache_dir = tmpdir.var.cache
45
47
  end
46
-
48
+
47
49
  teardown do
48
- Open.rm_rf tmpdir
50
+ begin
51
+ @scout_testing_state.restore if @scout_testing_state
52
+ ensure
53
+ if @saved_progress_severity
54
+ existed, value = @saved_progress_severity
55
+ if existed
56
+ Log::ProgressBar.instance_variable_set(:@default_severity, value)
57
+ elsif Log::ProgressBar.instance_variable_defined?(:@default_severity)
58
+ Log::ProgressBar.remove_instance_variable(:@default_severity)
59
+ end
60
+ end
61
+ Scout::Testing.remove_tmpdir(@tmpdir) if @tmpdir
62
+ end
49
63
  end
50
64
  end