expect-pty 0.5.3 → 0.6.1

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +28 -19
  4. data/docs/API.md +88 -0
  5. data/docs/MIGRATION.md +15 -0
  6. data/lib/expect/cleanup.rb +33 -0
  7. data/lib/expect/configuration.rb +87 -66
  8. data/lib/expect/interaction.rb +226 -209
  9. data/lib/expect/logging.rb +157 -156
  10. data/lib/expect/matcher.rb +30 -29
  11. data/lib/expect/pattern.rb +8 -2
  12. data/lib/expect/pattern_list.rb +35 -5
  13. data/lib/expect/redactor.rb +11 -4
  14. data/lib/expect/relay.rb +97 -73
  15. data/lib/expect/relay_writer.rb +2 -1
  16. data/lib/expect/result.rb +32 -8
  17. data/lib/expect/session.rb +508 -0
  18. data/lib/expect/session_resources.rb +10 -2
  19. data/lib/expect/terminal.rb +78 -78
  20. data/lib/expect/version.rb +1 -1
  21. data/lib/expect.rb +333 -520
  22. data/sig/expect.rbs +355 -0
  23. metadata +13 -106
  24. data/.rubocop.yml +0 -66
  25. data/CONTRIBUTING.md +0 -29
  26. data/Gemfile +0 -28
  27. data/Rakefile +0 -38
  28. data/benchmark/matching.rb +0 -98
  29. data/benchmark/redactor.rb +0 -64
  30. data/benchmark/relay.rb +0 -48
  31. data/benchmark/scaling.rb +0 -70
  32. data/benchmark/send_slow.rb +0 -41
  33. data/benchmark/support.rb +0 -114
  34. data/docs/COMPATIBILITY.md +0 -98
  35. data/docs/INTERNAL_CONTRACTS.md +0 -154
  36. data/docs/PERFORMANCE.md +0 -165
  37. data/docs/RELEASING.md +0 -84
  38. data/docs/VERIFICATION.md +0 -456
  39. data/examples/dialogue.rb +0 -30
  40. data/examples/kibitz/README.md +0 -81
  41. data/examples/kibitz/kibitz.rb +0 -142
  42. data/examples/kibitz/test_kibitz.rb +0 -37
  43. data/examples/ssh_auto.rb +0 -94
  44. data/examples/ssh_interact.rb +0 -159
  45. data/examples/ssh_login.rb +0 -64
  46. data/expect-pty.gemspec +0 -33
  47. data/script/ci +0 -121
  48. data/script/release.rb +0 -319
  49. data/test/buffer_accounting_test.rb +0 -83
  50. data/test/cleanup_test.rb +0 -251
  51. data/test/compare_upstream.rb +0 -157
  52. data/test/configuration_test.rb +0 -133
  53. data/test/deadline_test.rb +0 -237
  54. data/test/diagnostics_test.rb +0 -353
  55. data/test/edge_case_test.rb +0 -262
  56. data/test/fixtures/ssh_scripts/01_identity.sh +0 -4
  57. data/test/fixtures/ssh_scripts/02_output.sh +0 -5
  58. data/test/fixtures/ssh_scripts/03_delayed.sh +0 -6
  59. data/test/fixtures/ssh_scripts/04_failure.sh +0 -2
  60. data/test/fixtures/ssh_scripts/05_recovery.sh +0 -3
  61. data/test/initialization_failure_test.rb +0 -92
  62. data/test/integration/README.md +0 -109
  63. data/test/integration/ssh_scripts.rb +0 -94
  64. data/test/interact_test.rb +0 -253
  65. data/test/interconnect_test.rb +0 -425
  66. data/test/io_test.rb +0 -321
  67. data/test/kibitz_test.rb +0 -45
  68. data/test/lifecycle_contract_test.rb +0 -71
  69. data/test/literal_scan_test.rb +0 -97
  70. data/test/matching_test.rb +0 -211
  71. data/test/multi_session_test.rb +0 -66
  72. data/test/ownership_sequence_test.rb +0 -208
  73. data/test/pattern_offset_test.rb +0 -43
  74. data/test/process_interruption_test.rb +0 -296
  75. data/test/process_test.rb +0 -291
  76. data/test/redactor_test.rb +0 -183
  77. data/test/relay_recovery_test.rb +0 -451
  78. data/test/relay_reentrancy_test.rb +0 -159
  79. data/test/release_test.rb +0 -375
  80. data/test/ruby_api_test.rb +0 -515
  81. data/test/scan_reuse_test.rb +0 -109
  82. data/test/script_logging_test.rb +0 -124
  83. data/test/support/interact_probe.rb +0 -125
  84. data/test/support/kibitz_probe.rb +0 -177
  85. data/test/support/script_probe.rb +0 -158
  86. data/test/terminal_cleanup_test.rb +0 -349
  87. data/test/test_helper.rb +0 -58
  88. data/test/timeout_test.rb +0 -289
  89. data/test/write_contract_test.rb +0 -105
data/Rakefile DELETED
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rake/testtask"
4
-
5
- Rake::TestTask.new do |task|
6
- task.libs << "test"
7
- task.pattern = "test/**/*_test.rb"
8
- task.warning = true
9
- end
10
-
11
- desc "Check Ruby style and common mistakes"
12
- task :lint do
13
- ruby "-S", "rubocop"
14
- end
15
-
16
- task default: %i[lint test]
17
-
18
- namespace :test do
19
- desc "Log in over SSH and verify multiple scripts and persisted logs (opt-in)"
20
- task :ssh do
21
- ruby "test/integration/ssh_scripts.rb"
22
- end
23
-
24
- desc "Log in over SSH and verify real PTY interact handoff (opt-in)"
25
- task :ssh_interact do
26
- ruby "examples/ssh_interact.rb", "--auto"
27
- end
28
-
29
- desc "Automatically log into local SSH and run read-only macOS checks"
30
- task :ssh_auto do
31
- ruby "examples/ssh_auto.rb", "--no-interact"
32
- end
33
-
34
- desc "Run the local two-terminal kibitz scenarios and save logs (no SSH required)"
35
- task :kibitz do
36
- ruby "examples/kibitz/test_kibitz.rb"
37
- end
38
- end
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "support"
4
-
5
- runner = ExpectBenchmark::Runner.new("matching")
6
- reader, writer = IO.pipe
7
- session = Expect.open(reader, log_stdout: false)
8
- begin
9
- sizes = runner.smoke ? [4096] : [4096, 65_536, 1_048_576]
10
- sizes.product([1, 8, 32], %i[first last miss]).each do |size, count, hit|
11
- session.buffer = "#{"x" * (size - 3)}END"
12
- patterns = Array.new(count) { |index| /missing#{index}/ }
13
- index = hit == :first ? 0 : count - 1
14
- patterns[index] = /END/ unless hit == :miss
15
- matcher = Expect::Matcher.new(Expect::PatternList.new([session], patterns), 0)
16
- verify = lambda do |result|
17
- valid = if hit == :miss
18
- result.nil?
19
- else
20
- result && result[0].equal?(session) && result[1].number == index + 1 && result[2] == [size - 3, 3, []]
21
- end
22
- ExpectBenchmark.check(valid)
23
- end
24
- runner.measure("scan/#{size}/#{count}/#{hit}", bytes: size, inputs: { size: size, patterns: count, hit: hit },
25
- verify: verify) { matcher.__send__(:find_match) }
26
- end
27
- sizes.each do |size|
28
- prefix = "中" * (size / 3)
29
- session.buffer = "#{prefix}😀终tail"
30
- matcher = Expect::Matcher.new(Expect::PatternList.new([session], [/😀(终)(x)?/]), 0)
31
- verify = lambda do |result|
32
- ExpectBenchmark.check(result && result[2] == [prefix.bytesize, 7, ["终".b, nil]])
33
- end
34
- runner.measure("utf8/#{size}", bytes: session.buffer.bytesize, inputs: { prefix_bytes: prefix.bytesize },
35
- verify: verify) { matcher.__send__(:find_match) }
36
- end
37
-
38
- # 同一次等待不断追加新字节,分别对照字面与正则;跨块命中仍按声明优先级选择。
39
- (runner.smoke ? [4096] : [65_536, 1_048_576]).product(%i[literal regexp]).each do |size, kind|
40
- chunks = runner.smoke ? 4 : 32
41
- patterns = Array.new(32) { |index| "missing#{index}" }
42
- patterns[-1] = "END"
43
- patterns.map! { |value| Regexp.new(Regexp.escape(value)) } if kind == :regexp
44
- verify = lambda do |result|
45
- ExpectBenchmark.check(result && result[1].number == 32 && result[2] == [size + (chunks * 1024), 3, []])
46
- end
47
- runner.measure("stream/#{size}/#{kind}", bytes: size + (chunks * 1024) + 3,
48
- inputs: { initial_bytes: size, chunks: chunks, patterns: 32, kind: kind },
49
- iterations: 5, verify: verify) do
50
- session.buffer = "x" * size
51
- matcher = Expect::Matcher.new(Expect::PatternList.new([session], patterns), nil)
52
- ExpectBenchmark.check(matcher.__send__(:find_match).nil?)
53
- chunks.times do
54
- writer.write("x" * 1024)
55
- session.__send__(:read_available)
56
- ExpectBenchmark.check(matcher.__send__(:find_match).nil?)
57
- end
58
- writer.write("END")
59
- session.__send__(:read_available)
60
- matcher.__send__(:find_match)
61
- end
62
- end
63
- ensure
64
- session.close
65
- reader.close
66
- writer.close
67
- end
68
-
69
- [1, 8, 32].each do |count|
70
- pipes = Array.new(count) { IO.pipe }
71
- sessions = pipes.map { |input, _| Expect.open(input, log_stdout: false) }
72
- begin
73
- sessions.each { |source| source.buffer = "x" * 4096 }
74
- list = Expect::PatternList.new
75
- # 每轮交替反转来源组,确保同一会话真实出现在多个非相邻组。
76
- 8.times { |index| list.on(/missing/, from: index.even? ? sessions : sessions.reverse) }
77
- # 单会话的相邻相同组会合并;保持普通单会话路径作为对照。
78
- matcher = Expect::Matcher.new(list, 0)
79
- runner.measure("groups/#{count}", bytes: 4096 * count, inputs: { sessions: count, groups: list.groups.size },
80
- verify: lambda { |result|
81
- ExpectBenchmark.check(result.nil?)
82
- }) { matcher.__send__(:find_match) }
83
- matcher.run
84
- verify = lambda do |result|
85
- ExpectBenchmark.check(result == :retry && sessions.all? { |source| source.buffer == "r" })
86
- end
87
- runner.measure("ready/#{count}", bytes: count, inputs: { sessions: count }, verify: verify) do
88
- sessions.each(&:clear_buffer)
89
- pipes.each { |pipe| pipe.last.write("r") }
90
- ready = IO.select(pipes.map(&:first), nil, nil, 0).first
91
- matcher.__send__(:read_ready, ready, sessions)
92
- end
93
- ensure
94
- sessions.each(&:close)
95
- pipes.flatten.each(&:close)
96
- end
97
- end
98
- runner.finish
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "support"
4
-
5
- # 同一驱动可加载基线或候选库;每轮创建独立过滤器,不能跨样本继承流状态。
6
- module RedactorBenchmark
7
- def self.measure(runner, name, input, patterns, expected, chunk_size: nil, partial: false)
8
- chunks = if chunk_size
9
- (0...input.bytesize).step(chunk_size).map { |offset| input.byteslice(offset, chunk_size) }
10
- else
11
- [input]
12
- end
13
- runner.measure(name, bytes: input.bytesize,
14
- inputs: { bytes: input.bytesize, secret_lengths: patterns.map(&:bytesize),
15
- chunk_size: chunk_size, partial: partial, repeated_finish: true },
16
- verify: ->(output) { ExpectBenchmark.check(output == expected, "incorrect #{name} output") },
17
- iterations: 5) do
18
- filter = Expect::Redactor.new(patterns)
19
- output = +"".b
20
- chunks.each { |chunk| output << filter.append(chunk) }
21
- output << filter.finish(partial: partial) << filter.finish(partial: partial)
22
- end
23
- end
24
- end
25
-
26
- runner = ExpectBenchmark::Runner.new("redactor")
27
- size = runner.smoke ? 512 : 65_536
28
- plain = "x" * size
29
- RedactorBenchmark.measure(runner, "empty_rules", plain, [], plain)
30
- RedactorBenchmark.measure(runner, "no_match", plain, ["secret"], plain)
31
- RedactorBenchmark.measure(runner, "no_match_many", plain, %w[secret password token credential], plain)
32
- RedactorBenchmark.measure(runner, "sparse", "#{plain}secret!", ["secret"], "#{plain}[FILTERED]!")
33
- RedactorBenchmark.measure(runner, "contiguous_short", "token" * (size / 5), ["token"], "[FILTERED]")
34
-
35
- (runner.smoke ? [1, 32, 128] : [1, 32, 1024, 4096]).each do |length|
36
- RedactorBenchmark.measure(runner, "overlap_#{length}", "a" * size, ["a" * length], "[FILTERED]")
37
- end
38
- count = runner.smoke ? 8 : 256
39
- RedactorBenchmark.measure(runner, "contained_patterns", "abcde!" * count,
40
- %w[abc bc ab bcd], "[FILTERED]e!" * count)
41
- RedactorBenchmark.measure(runner, "binary_overlap", "\0\xffABC\x80!".b * count,
42
- ["\xffABC".b, "ABC\x80".b], "\0[FILTERED]!".b * count, chunk_size: 3)
43
- RedactorBenchmark.measure(runner, "marker_in_input", "[FILTERED] secret!" * count,
44
- %w[secret FILTERED], "[[FILTERED]] [FILTERED]!" * count)
45
-
46
- [1, 7, 4096].each do |chunk_size|
47
- RedactorBenchmark.measure(runner, "stream_#{chunk_size}", "prefix secret!\n" * count,
48
- ["secret"], "prefix [FILTERED]!\n" * count, chunk_size: chunk_size)
49
- end
50
- [true, false].each do |partial|
51
- RedactorBenchmark.measure(runner, "partial_#{partial}", "prefix pass", ["password"],
52
- partial ? "prefix [FILTERED]" : "prefix pass", chunk_size: 1, partial: partial)
53
- end
54
-
55
- runner.measure("update_pending", bytes: 10, inputs: { update: "keep old mask and match new secret" },
56
- verify: lambda { |output|
57
- ExpectBenchmark.check(output == "[FILTERED]!")
58
- }, iterations: 100) do
59
- filter = Expect::Redactor.new(%w[abc long-pattern])
60
- output = filter.append("abcsec")
61
- filter.patterns = ["secret"]
62
- output << filter.append("ret!") << filter.finish << filter.finish
63
- end
64
- runner.finish
data/benchmark/relay.rb DELETED
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "support"
4
-
5
- runner = ExpectBenchmark::Runner.new("relay")
6
- reader, writer = IO.pipe
7
- session = Expect.open(reader, log_stdout: false)
8
- begin
9
- size = runner.smoke ? 4096 : 65_536
10
- payload = "x" * size
11
- %i[none literal regexps].each do |kind|
12
- session.__send__(:sequences=, {})
13
- session.on_sequence("STOP") if kind == :literal
14
- 16.times { |index| session.on_sequence(/missing#{index}/) } if kind == :regexps
15
- output = StringIO.new("".b)
16
- session.listeners = [output]
17
- verify = ->(result) { ExpectBenchmark.check(result == true && output.string == payload) }
18
- runner.measure("escape/#{kind}", bytes: size, inputs: { size: size, regexps: kind == :regexps ? 16 : 0 },
19
- verify: verify) do
20
- output.string = "".b
21
- session.__send__(:relay_history).clear
22
- Expect.__send__(:relay_buffer, session, { session => payload.b })
23
- end
24
- end
25
-
26
- session.__send__(:sequences=, {})
27
- writer.close
28
- normal = StringIO.new("".b)
29
- slow = StringIO.new("".b)
30
- slow.define_singleton_method(:write) { |data| super(data.byteslice(0, 17)) }
31
- session.listeners = [normal, slow]
32
- verify = lambda do |result|
33
- ExpectBenchmark.check(result.equal?(session) && normal.string == payload && slow.string == payload)
34
- ExpectBenchmark.check(!session.pending_output? && session.buffer.empty?)
35
- end
36
- runner.measure("mixed_targets", bytes: size * 2, inputs: { size: size, short_write: 17 }, iterations: 5,
37
- verify: verify) do
38
- normal.string = "".b
39
- slow.string = "".b
40
- session.buffer = payload
41
- Timeout.timeout(10) { Expect.interconnect(session, timeout: 5) }
42
- end
43
- ensure
44
- session.close
45
- reader.close
46
- writer.close unless writer.closed?
47
- end
48
- runner.finish
data/benchmark/scaling.rb DELETED
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "support"
4
-
5
- counts = [1, 16, 64]
6
- runner = ExpectBenchmark::Runner.new("scaling") do |options|
7
- options.on("--sessions N", Integer, "Measure one explicit session count (check the process fd limit)") do |count|
8
- raise ArgumentError, "sessions must be positive" unless count.positive?
9
-
10
- counts = [count]
11
- end
12
- end
13
- counts = [1, 8] if runner.smoke
14
- counts.each do |count|
15
- if (count * 2) + 32 > Process.getrlimit(:NOFILE).first
16
- raise ArgumentError, "#{count} sessions need a larger process fd limit; run ulimit -n before this benchmark"
17
- end
18
-
19
- pipes = []
20
- sessions = []
21
- begin
22
- count.times do
23
- pipes << IO.pipe
24
- sessions << Expect.open(pipes.last.first)
25
- end
26
- marker = "ready\n"
27
- ready_count = 0
28
- list = Expect::PatternList.new(sessions)
29
- list.on(marker) do
30
- ready_count += 1
31
- Expect.continue(reset_timeout: false) if ready_count < count
32
- end
33
- verify = lambda do |result|
34
- ExpectBenchmark.check(result.matched? && ready_count == count && sessions.all? { |source| source.buffer.empty? })
35
- end
36
- runner.measure("all_ready/#{count}", bytes: count * marker.bytesize, inputs: { sessions: count },
37
- iterations: 10, verify: verify) do
38
- ready_count = 0
39
- pipes.each { |pipe| pipe.last.write(marker) }
40
- Timeout.timeout(10) { Expect::Matcher.new(list, 5).run }
41
- end
42
- ensure
43
- sessions.each(&:close)
44
- pipes.flatten.each { |io| io.close unless io.closed? }
45
- end
46
- end
47
-
48
- # 真实管道填满后完全不消费;另一个来源的标记仍须被读到,重复进入不能增长描述符。
49
- pipes = Array.new(3) { IO.pipe }
50
- slow = Expect.open(pipes[0].first)
51
- fast = Expect.open(pipes[1].first)
52
- sink = pipes[2].last
53
- begin
54
- loop { break if sink.write_nonblock("x" * 4096, exception: false) == :wait_writable }
55
- slow.listeners = [sink]
56
- fast.on_sequence("PROBE")
57
- verify = lambda do |result|
58
- ExpectBenchmark.check(result.equal?(fast) && slow.pending_output? && fast.buffer.empty?)
59
- end
60
- runner.measure("blocked_target_probe", bytes: 5, inputs: { sources: 2, blocked_targets: 1 },
61
- iterations: 100, verify: verify) do
62
- slow.buffer = "blocked"
63
- pipes[1].last.write("PROBE")
64
- Timeout.timeout(10) { Expect.interconnect(slow, fast, timeout: 5) }
65
- end
66
- ensure
67
- [slow, fast].each(&:close)
68
- pipes.flatten.each { |io| io.close unless io.closed? }
69
- end
70
- runner.finish
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "support"
4
-
5
- runner = ExpectBenchmark::Runner.new("send_slow")
6
- size = runner.smoke ? 8 : 128
7
- payload = "x" * size
8
- [false, true].product([0, 0.001]).each do |echo, delay|
9
- verify = lambda do |result|
10
- count, received, reply = result
11
- ExpectBenchmark.check(count == size && received == payload && reply == (echo ? payload : nil))
12
- end
13
- runner.measure("send/#{echo ? "echo" : "silent"}/#{delay}", bytes: size,
14
- inputs: { size: size, echo: echo, delay: delay },
15
- iterations: 1, verify: verify) do
16
- client, peer = Socket.pair(:UNIX, :STREAM, 0)
17
- session = Expect.open(client, log_stdout: false)
18
- consumer = Thread.new do
19
- received = "".b
20
- while received.bytesize < size
21
- data = peer.readpartial(size)
22
- received << data
23
- peer.write(data) if echo
24
- end
25
- received
26
- end
27
- begin
28
- Timeout.timeout(10) do
29
- count = session.send_slow(payload, delay: delay)
30
- reply = session.expect_result(payload, timeout: 2).match if echo
31
- [count, consumer.value, reply]
32
- end
33
- ensure
34
- consumer.kill.join
35
- session.close
36
- client.close
37
- peer.close
38
- end
39
- end
40
- end
41
- runner.finish
data/benchmark/support.rb DELETED
@@ -1,114 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "fileutils"
5
- require "optparse"
6
- require "open3"
7
- require "digest"
8
- require "socket"
9
- require "timeout"
10
-
11
- # 标准库基准入口;每个工作负载先验证结果,采样后再核对,计时不包含断言。
12
- module ExpectBenchmark
13
- # 保存环境、输入规模、耗时、分配与 GC 样本,便于比较不同源码目录。
14
- class Runner
15
- attr_reader :smoke, :library
16
-
17
- def initialize(name)
18
- @library = File.expand_path("../lib", __dir__)
19
- @samples = 5
20
- @iterations = nil
21
- @smoke = false
22
- @output = "tmp/benchmark/#{name}.json"
23
- OptionParser.new do |options|
24
- options.banner = "Usage: ruby benchmark/#{name}.rb [options]"
25
- options.on("--smoke", "Small correctness workload") { @smoke = true }
26
- options.on("--library PATH", "Compare another checkout's lib directory") do |path|
27
- @library = File.expand_path(path)
28
- end
29
- options.on("--samples N", Integer) { |count| @samples = count }
30
- options.on("--iterations N", Integer) { |count| @iterations = count }
31
- options.on("--output PATH") { |path| @output = path }
32
- yield options if block_given?
33
- end.parse!
34
- unless @samples.positive? && (!@iterations || @iterations.positive?)
35
- raise ArgumentError, "counts must be positive"
36
- end
37
-
38
- require File.join(@library, "expect")
39
- @results = []
40
- end
41
-
42
- def measure(name, bytes:, inputs:, verify:, iterations: 100, &operation)
43
- iterations = @iterations || (smoke ? 1 : iterations)
44
- samples = smoke ? 1 : @samples
45
- verify.call(operation.call) # 同时预热;错误输出永远不能成为更快的样本。
46
- measurements = Array.new(samples) do
47
- GC.start
48
- resources_before = resources
49
- before = GC.stat
50
- started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
51
- result = nil
52
- iterations.times { result = operation.call }
53
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
54
- after = GC.stat
55
- verify.call(result)
56
- {
57
- seconds: elapsed, allocated_objects: after[:total_allocated_objects] - before[:total_allocated_objects],
58
- gc_count: after[:count] - before[:count], processed_bytes: bytes * iterations,
59
- resources_before: resources_before, resources_after: resources
60
- }
61
- end
62
- @results << { name: name, inputs: inputs, iterations: iterations, samples: measurements }
63
- puts "#{name}: #{format("%.6f", measurements.map { |row| row[:seconds] }.sort[samples / 2])}s"
64
- end
65
-
66
- def finish
67
- root = File.dirname(library)
68
- repository = git_output(root, "rev-parse", "--show-toplevel")&.strip
69
- if repository && File.realpath(repository) == File.realpath(root)
70
- sha = git_output(root, "rev-parse", "HEAD")
71
- dirty = git_output(root, "status", "--porcelain", "--untracked-files=all")
72
- end
73
- digest = Digest::SHA256.new
74
- Dir[File.join(library, "**/*.rb")].each do |path|
75
- digest << path.delete_prefix(library) << File.binread(path)
76
- end
77
- report = {
78
- ruby: RUBY_DESCRIPTION, platform: RUBY_PLATFORM, revision: sha&.strip,
79
- fd_limit: Process.getrlimit(:NOFILE).first,
80
- dirty: dirty.nil? ? nil : !dirty.empty?, library_sha256: digest.hexdigest, smoke: smoke, results: @results
81
- }
82
- FileUtils.mkdir_p(File.dirname(@output))
83
- File.write(@output, "#{JSON.pretty_generate(report)}\n")
84
- puts "Saved #{@output}"
85
- end
86
-
87
- private
88
-
89
- # 资源采样在计时区间之外;RSS 是端点值,不冒充峰值。目录不可用时显式记录 nil。
90
- def resources
91
- directory = File.directory?("/proc/self/fd") ? "/proc/self/fd" : "/dev/fd"
92
- descriptors = Dir.children(directory).size if File.directory?(directory)
93
- rss = if File.file?("/proc/self/status")
94
- File.read("/proc/self/status")[/^VmRSS:\s+(\d+)/, 1]&.to_i
95
- else
96
- output, _, status = Open3.capture3("ps", "-o", "rss=", "-p", Process.pid.to_s)
97
- output.to_i if status.success?
98
- end
99
- { rss_bytes: rss && (rss * 1024), descriptors: descriptors }
100
- end
101
-
102
- # 安装包和源码归档可能没有 Git;未知状态记录为 nil,不误报为干净提交。
103
- def git_output(root, *)
104
- output, _, status = Open3.capture3("git", "-C", root, *)
105
- output if status.success?
106
- rescue Errno::ENOENT
107
- nil
108
- end
109
- end
110
-
111
- def self.check(condition, message = "incorrect benchmark output")
112
- raise message unless condition
113
- end
114
- end
@@ -1,98 +0,0 @@
1
- # Ruby 接口与 Expect 行为对照
2
-
3
- ## Tcl Expect 语义边界
4
-
5
- 参考 Tcl Expect 的[官方手册](https://core.tcl-lang.org/expect/doc/trunk/expect.man)、
6
- [`expect.c`](https://core.tcl-lang.org/expect/raw/expect.c?ci=trunk) 的 `eval_cases` / `expMatchProcess` / 计时循环,
7
- 以及 [`exp_inter.c`](https://core.tcl-lang.org/expect/raw/exp_inter.c?ci=trunk) 的 `intMatch` 与终端恢复流程。
8
- 本库借鉴交互模型,保留以下明确差异,不承诺 Tcl 脚本或完整功能兼容。
9
-
10
- | Tcl Expect 能力 | 本库的 Ruby 表达与边界 |
11
- |---------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
12
- | `spawn`、`send`、`expect`、`interact` | PTY 会话、`write` / `puts`、原生模式块和人工接管;`send` 保留 Ruby 反射语义 |
13
- | `exp_continue` / `-continue_timer` | `continue` / `continue(reset_timeout: false)`;使用单调时钟和秒数关键字参数 |
14
- | 匹配后消费输入、`-notransfer` | `before` / `match` / `after` 与会话级 `preserve_buffer`;无进展回调不会原地重复执行 |
15
- | glob、exact、regexp 模式 | 字符串只作字面匹配,正则采用 Ruby `Regexp`;锚点和多行行为遵守 Ruby |
16
- | `match_max`、`full_buffer` | `buffer_limit` 只保留尾部字节,默认无限;`buffer_discarded_bytes` 统计累计裁剪量,无缓冲满事件 |
17
- | 前置/后置模式、后台匹配 | 没有全局隐式规则或后台读取器;调用方显式组合模式块,每个会话由一个读取者驱动 |
18
- | `interact` 的部分正则匹配 | Tcl 的 `CANMATCH` 可以暂存潜在匹配;Ruby 原生正则不提供该接口,本库使用有限历史窗口,已转发前缀不能撤回。需要完整过滤时使用字面转义 |
19
- | `close` 与 `wait` | IO 结束与进程退出仍分开判断;本库的块生命周期和 `close` 会额外回收直属子进程 |
20
-
21
- 长输出任务应通过日志或监听器流式保存,并按提示长度设置 `buffer_limit`。只读 `buffer_discarded_bytes` 提供会话累计裁剪字节数,正常匹配消费和清空不计入;它不是无损缓冲满事件。后台匹配与共享前置/后置模式也需要独立的取消、优先级和资源归属约定,尚未实现。
22
-
23
- `expect` / `expect_result` 的 `deadline:` 是额外的绝对单调时钟总期限,限制相对 `timeout` 的所有重置,不改变已知 EOF 派发和未指定总期限时的零超时轮询。会话 `diagnostic_output` 与接收日志独立;显式 `redact` 仅过滤日志和诊断,协议转发与匹配仍使用原始字节。
24
-
25
- ## Expect.pm 接口迁移
26
-
27
- 交互能力参考 [jacoby/expect.pm](https://github.com/jacoby/expect.pm),源码基准为版本 1.38、提交
28
- `2ea0e4ce20a896c95cb4c94e781f1b1f3145150d`。此项目独立实现,使用 MIT 许可,没有复制 Perl 实现代码;原项目作者及维护者为
29
- Austin Schutz、Roland Giersig、Dave Jacoby,采用与 Perl 相同的许可。
30
-
31
- 从 0.2.0 起采用 Ruby 原生接口。下表列出迁移关系, **旧方法、别名和参数语法已移除**;历史 0.1.1 安装包的接口见对应版本记录。
32
-
33
- | 原接口或状态 | 当前 Ruby 接口 |
34
- |--------------------------------------------|---------------------------------------------------------------------|
35
- | 包级默认值、`Expect.defaults` | `Expect.configure { \|config\| ... }`、冻结的 `Expect.configuration` |
36
- | `timeout(10)` 等读写合一方法 | `timeout`、`timeout = 10`;布尔查询使用 `name?` |
37
- | `exp_init`、`init` | `Expect.open(io, writer:, own:)`,支持块生命周期 |
38
- | `expect(seconds, ...)` | `expect(..., timeout: seconds)`;完整结果使用 `expect_result` |
39
- | `-ex`、`-re`、数组字符串正则 | 字符串字面匹配,正则使用原生 `Regexp` |
40
- | `-i` 分组 | `from: session` / `from: [sessions]`,或块内 `on(..., from:)` |
41
- | `[pattern, callback, *args]` | `on(pattern) { \|session\| ... }`,附加参数使用闭包 |
42
- | `exp_continue`、`exp_continue_timeout` | `continue`、`continue(reset_timeout: false)` |
43
- | 数字前缀错误字符串 | `Result#error` 为 `:timeout`、`:eof` 或原始 IO 异常对象 |
44
- | `matchlist`、`exp_matchlist` | `captures` |
45
- | `exp_pid`、`exp_match` 等别名 | `pid`、`match` 等属性 |
46
- | `get_accum`、`set_accum`、`clear_accum` | `buffer`、`buffer=`、`clear_buffer` |
47
- | `max_accum`、`match_max` | `buffer_limit`,正整数;`nil` 表示无限 |
48
- | `notransfer` | `preserve_buffer` |
49
- | `restart_timeout_upon_receive` | `reset_timeout_on_read` |
50
- | `log_user`、`log_group` | `log_stdout`、`log_listeners` |
51
- | `log_file`、`logfile`、`exp_logfile` | `log_output` / `log_output=`;路径和日志块使用 `log_to(..., mode:)` |
52
- | `print_log_file` | `write_log` |
53
- | `set_group` | `listeners` / `listeners=` |
54
- | `set_seq(sequence, callback, args)` | `on_sequence(sequence) { ... }` |
55
- | `send`、`print` | `write` 原样写入、`puts` 按行输出;`send` 保留 Ruby 反射语义 |
56
- | `send_slow(delay, *strings)`、`write_slow` | `send_slow(*objects, delay:)` |
57
- | `manual_stty = true` | `raw_terminal = false`;`stty` 保留,`exp_stty` 移除 |
58
- | `interact(input, escape)` | `interact(input:, escape:, output:, timeout:)` |
59
- | `test_handles` 的索引数组 | `readable_sessions(*sessions, timeout:)` 返回会话对象数组 |
60
- | `wait(seconds)` | `wait(timeout: seconds)`,返回 `Process::Status` |
61
- | `exitstatus`、`exp_exitstatus` 原始整数 | `process_status.to_i`;普通退出码用 `exit_code` |
62
- | `do_soft_close` | `graceful_close`,可通过 `close(graceful:)` 单次覆盖 |
63
- | `debug`、`exp_internal` | `debug_level`,整数 `0..3` |
64
- | `ttyname`、`pty_handle` | `tty_name`、标准 `inspect` |
65
- | `version` | 常量 `Expect::VERSION` |
66
- | `multiline_matching` | 原生正则的 `^` / `$`、`\A` / `\z` 和 `/m` |
67
- | `ignore_eintr` | 匹配等待自动重试 EINTR,并保留原期限 |
68
-
69
- ## 保留的交互能力
70
-
71
- 仍支持真实控制终端、精确/正则匹配、模式优先级、捕获组、二进制与分片
72
- UTF-8、EOF/超时回调、绝对期限与接收重置、多会话、缓冲上限、慢速写入和背压、路径/IO/回调日志、监听组、人工接管、跨读取转义及终端恢复。
73
-
74
- `expect` 返回模式序号或 `nil`;`expect_result` / `last_result` 返回原生七字段 `Struct`。`to_a`、`to_h`、模式解构使用 Ruby
75
- 自带行为,多重赋值需要显式 `result.to_a`。日志按实际读取时记录,转接与匹配切换不重复写日志;显式启用 `redact` 时过滤已注册秘密。
76
-
77
- ## 软关闭与硬关闭
78
-
79
- 参考原版的策略边界:`soft_close` 先收集剩余输出,关闭所属句柄,等待退出并最多发送 TERM; **不发送 KILL**。未退出返回 `nil`,保留
80
- PID。`hard_close` 不等待输出,必要时 TERM、KILL 并回收子进程。关闭会话 IO 不代表子进程已经退出。
81
-
82
- Ruby 使用关键字指定各阶段期限,关闭方法返回 `Process::Status` 或 `nil`。`close(graceful: true)` 和 `graceful_close = true`
83
- 先尝试软关闭,再在 `ensure` 中硬关闭;这对应原版销毁时可选软关闭、随后硬关闭的清理策略。`close` 返回 `nil`;GC
84
- 兜底直接强制清理,不阻塞等待。
85
-
86
- ## 有意采用的 Ruby 语义
87
-
88
- - 默认关闭 stdout 输出,显式启用 `log_stdout`。配置按会话隔离,构造参数覆盖全局默认;配置异常不发布部分状态。
89
- - 仅 `nil` 和 `false` 为假,`0` 为真。转接回调也采用此规则。
90
- - 字符串始终是字面文本;只有 `:eof` / `:timeout` 是事件,`"EOF"` 是普通转义文本。
91
- - 超时回调接收所有仍在监听的会话;重复注册超时回调抛出 `ArgumentError`。
92
- - `write` 返回字节数,`puts` 返回 `nil`,`<<` 返回会话。
93
- - 启动失败抛出 `Expect::SpawnError` 并回收;EOF 不主动终止仍存活的进程。借用 IO 不关闭,接管 IO 在初始化失败或会话关闭时释放。
94
- - Perl 特有的正则、IO::Pty 继承 API、全局信号 handler 和 Solaris 字节删除补丁不移植。Ruby 使用 `to_io`、`slave`、`io/console`
95
- 和原生正则。
96
-
97
- `test/compare_upstream.rb` 仅对共同的交互行为作可选差分验证,通过 Ruby 新接口表达同样场景,并显式转换错误标记和可读会话索引;它不要求或证明旧
98
- API 兼容,也不等于运行完整上游测试套件。