yaic 0.1.0 → 0.2.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/.claude/agents/ralph-qa.md +101 -0
- data/.claude/ralph/bin/dump-pid.sh +3 -0
- data/.claude/ralph/bin/kill-claude +6 -0
- data/.claude/ralph/bin/start-ralph +44 -0
- data/.claude/ralph/bin/stop-hook.sh +9 -0
- data/.claude/ralph/bin/stop-ralph +17 -0
- data/.claude/ralph/prompt.md +218 -0
- data/.claude/settings.json +26 -0
- data/.gitmodules +3 -0
- data/CLAUDE.md +65 -0
- data/README.md +106 -17
- data/Rakefile +8 -0
- data/devenv.nix +1 -0
- data/docs/agents/data-model.md +150 -0
- data/docs/agents/ralph/features/01-message-parsing.md.done +160 -0
- data/docs/agents/ralph/features/01-tcpsocket-refactor.md.done +109 -0
- data/docs/agents/ralph/features/02-connection-socket.md.done +138 -0
- data/docs/agents/ralph/features/02-simplified-client-api.md.done +306 -0
- data/docs/agents/ralph/features/03-registration.md.done +147 -0
- data/docs/agents/ralph/features/04-ping-pong.md.done +109 -0
- data/docs/agents/ralph/features/05-event-system.md.done +167 -0
- data/docs/agents/ralph/features/06-privmsg-notice.md.done +163 -0
- data/docs/agents/ralph/features/07-join-part.md.done +190 -0
- data/docs/agents/ralph/features/08-quit.md.done +118 -0
- data/docs/agents/ralph/features/09-nick-change.md.done +109 -0
- data/docs/agents/ralph/features/10-topic.md.done +145 -0
- data/docs/agents/ralph/features/11-kick.md.done +122 -0
- data/docs/agents/ralph/features/12-names.md.done +124 -0
- data/docs/agents/ralph/features/13-mode.md.done +174 -0
- data/docs/agents/ralph/features/14-who-whois.md.done +188 -0
- data/docs/agents/ralph/features/15-client-api.md.done +180 -0
- data/docs/agents/ralph/features/16-ssl-test-infrastructure.md.done +50 -0
- data/docs/agents/ralph/features/17-github-actions-ci.md.done +70 -0
- data/docs/agents/ralph/features/18-brakeman-security-scanning.md.done +67 -0
- data/docs/agents/ralph/features/19-fix-qa.md.done +73 -0
- data/docs/agents/ralph/features/20-test-optimization.md.done +70 -0
- data/docs/agents/ralph/features/21-test-parallelization.md.done +56 -0
- data/docs/agents/ralph/features/22-wait-until-pattern.md.done +90 -0
- data/docs/agents/ralph/features/23-ping-test-optimization.md.done +46 -0
- data/docs/agents/ralph/features/24-blocking-who-whois.md.done +159 -0
- data/docs/agents/ralph/features/25-verbose-mode.md.done +166 -0
- data/docs/agents/ralph/plans/test-optimization-plan.md +172 -0
- data/docs/agents/ralph/progress.md +731 -0
- data/docs/agents/todo.md +5 -0
- data/lib/yaic/channel.rb +22 -0
- data/lib/yaic/client.rb +821 -0
- data/lib/yaic/event.rb +35 -0
- data/lib/yaic/message.rb +119 -0
- data/lib/yaic/registration.rb +17 -0
- data/lib/yaic/socket.rb +120 -0
- data/lib/yaic/source.rb +39 -0
- data/lib/yaic/version.rb +1 -1
- data/lib/yaic/who_result.rb +17 -0
- data/lib/yaic/whois_result.rb +20 -0
- data/lib/yaic.rb +13 -1
- metadata +51 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Test Parallelization
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Enable minitest's built-in parallel test execution to run tests concurrently, reducing total test time by utilizing multiple CPU cores.
|
|
6
|
+
|
|
7
|
+
## Behavior
|
|
8
|
+
|
|
9
|
+
### Enable Parallel Execution
|
|
10
|
+
|
|
11
|
+
Add `parallelize_me!` to test classes or enable globally in test_helper.rb.
|
|
12
|
+
|
|
13
|
+
### Fix Unique Identifiers for Thread Safety
|
|
14
|
+
|
|
15
|
+
Tests currently use `Process.pid` for unique nicks/channels, but with thread-based parallelism all threads share the same PID. Update identifier generation to include thread ID:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
# Before
|
|
19
|
+
@test_nick = "t#{Process.pid}#{Time.now.to_i % 10000}"
|
|
20
|
+
|
|
21
|
+
# After
|
|
22
|
+
@test_nick = "t#{Process.pid}_#{Thread.current.object_id % 10000}_#{rand(10000)}"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Verify Test Isolation
|
|
26
|
+
|
|
27
|
+
Ensure tests don't share state:
|
|
28
|
+
- Each test creates its own IRC client connections
|
|
29
|
+
- Each test uses unique channel names
|
|
30
|
+
- No global variables or class variables modified during tests
|
|
31
|
+
|
|
32
|
+
## Tests
|
|
33
|
+
|
|
34
|
+
Run the full test suite and verify:
|
|
35
|
+
- All 267 tests pass
|
|
36
|
+
- All 575 assertions pass
|
|
37
|
+
- No new skips introduced
|
|
38
|
+
- Test time is reduced (target: 3-4x speedup)
|
|
39
|
+
|
|
40
|
+
## Implementation Notes
|
|
41
|
+
|
|
42
|
+
- Use `bundle exec rake test` to run tests
|
|
43
|
+
- Use `bundle exec standardrb -A` for linting
|
|
44
|
+
- The IRC server can handle multiple concurrent connections
|
|
45
|
+
|
|
46
|
+
Minitest parallel options:
|
|
47
|
+
1. `parallelize_me!` in individual test classes
|
|
48
|
+
2. Global enable in test_helper.rb via `Minitest::Test.parallelize_me!`
|
|
49
|
+
|
|
50
|
+
## Dependencies
|
|
51
|
+
|
|
52
|
+
- 20-test-optimization.md (planning complete)
|
|
53
|
+
|
|
54
|
+
## Reference
|
|
55
|
+
|
|
56
|
+
See `docs/agents/ralph/plans/test-optimization-plan.md` for full analysis.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Wait Until Pattern
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Replace `sleep 0.5`, `read_multiple`, `read_until_pong`, and similar patterns with a consistent `wait_until` helper that returns as soon as a condition is met.
|
|
6
|
+
|
|
7
|
+
## Behavior
|
|
8
|
+
|
|
9
|
+
### Create wait_until Helper
|
|
10
|
+
|
|
11
|
+
Add a `wait_until` helper to test_helper.rb:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
def wait_until(timeout: 2)
|
|
15
|
+
deadline = Time.now + timeout
|
|
16
|
+
until Time.now > deadline
|
|
17
|
+
result = yield
|
|
18
|
+
return result if result
|
|
19
|
+
sleep 0.01
|
|
20
|
+
end
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Replace sleep Statements
|
|
26
|
+
|
|
27
|
+
Replace `sleep 0.5` with `wait_until { condition }`:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
# Before
|
|
31
|
+
client1.kick(@test_channel, @test_nick2)
|
|
32
|
+
sleep 0.5
|
|
33
|
+
assert kick_received
|
|
34
|
+
|
|
35
|
+
# After
|
|
36
|
+
client1.kick(@test_channel, @test_nick2)
|
|
37
|
+
wait_until { kick_received }
|
|
38
|
+
assert kick_received
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Replace read_multiple Pattern
|
|
42
|
+
|
|
43
|
+
Replace `read_multiple(socket, 5)` with inline `wait_until`:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
# Before
|
|
47
|
+
messages = read_multiple(socket, 5)
|
|
48
|
+
erroneous = messages.find { |m| m.command == "432" }
|
|
49
|
+
|
|
50
|
+
# After
|
|
51
|
+
erroneous = nil
|
|
52
|
+
wait_until(timeout: 2) do
|
|
53
|
+
raw = socket.read
|
|
54
|
+
if raw
|
|
55
|
+
msg = Yaic::Message.parse(raw)
|
|
56
|
+
erroneous = msg if msg&.command == "432"
|
|
57
|
+
end
|
|
58
|
+
erroneous
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Remove Old Helpers
|
|
63
|
+
|
|
64
|
+
Remove these helpers from test files:
|
|
65
|
+
- `read_multiple`
|
|
66
|
+
- `read_until_pong`
|
|
67
|
+
- `read_until_welcome`
|
|
68
|
+
- `read_with_timeout`
|
|
69
|
+
|
|
70
|
+
## Tests
|
|
71
|
+
|
|
72
|
+
Run the full test suite and verify:
|
|
73
|
+
- All tests pass
|
|
74
|
+
- No assertions removed or weakened
|
|
75
|
+
- Test time is reduced
|
|
76
|
+
|
|
77
|
+
## Implementation Notes
|
|
78
|
+
|
|
79
|
+
- Use `bundle exec rake test` to run tests
|
|
80
|
+
- Use `bundle exec standardrb -A` for linting
|
|
81
|
+
- Focus on integration tests first (they have the most sleeps)
|
|
82
|
+
|
|
83
|
+
## Dependencies
|
|
84
|
+
|
|
85
|
+
- 20-test-optimization.md (planning complete)
|
|
86
|
+
- 21-test-parallelization.md (optional, can be done in parallel)
|
|
87
|
+
|
|
88
|
+
## Reference
|
|
89
|
+
|
|
90
|
+
See `docs/agents/ralph/plans/test-optimization-plan.md` for full analysis.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Ping Test Optimization
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Optimize the `test_client_automatically_responds_to_server_ping` test which currently takes 7 seconds due to a `sleep 6` waiting for the server's PING.
|
|
6
|
+
|
|
7
|
+
## Behavior
|
|
8
|
+
|
|
9
|
+
### Reduce Server Ping Frequency
|
|
10
|
+
|
|
11
|
+
Update the InspIRCd test config (`test/fixtures/inspircd.conf`) to reduce ping frequency from 5 seconds to 3 seconds.
|
|
12
|
+
|
|
13
|
+
### Use wait_until Instead of Fixed Sleep
|
|
14
|
+
|
|
15
|
+
Replace the `sleep 6` with a `wait_until` that detects when a PING/PONG cycle has occurred:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
# Before
|
|
19
|
+
sleep 6
|
|
20
|
+
assert client.last_received_at > initial_last_received
|
|
21
|
+
|
|
22
|
+
# After
|
|
23
|
+
wait_until(timeout: 5) { client.last_received_at > initial_last_received }
|
|
24
|
+
assert client.last_received_at > initial_last_received
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Tests
|
|
28
|
+
|
|
29
|
+
Run the ping pong integration tests and verify:
|
|
30
|
+
- All tests pass
|
|
31
|
+
- `test_client_automatically_responds_to_server_ping` completes in ~3-4 seconds instead of 7
|
|
32
|
+
|
|
33
|
+
## Implementation Notes
|
|
34
|
+
|
|
35
|
+
- Use `bundle exec m test/integration/ping_pong_test.rb` to run just ping tests
|
|
36
|
+
- Use `bundle exec standardrb -A` for linting
|
|
37
|
+
- After changing server config, restart the IRC server: `bin/stop-irc-server && bin/start-irc-server`
|
|
38
|
+
|
|
39
|
+
## Dependencies
|
|
40
|
+
|
|
41
|
+
- 20-test-optimization.md (planning complete)
|
|
42
|
+
- 22-wait-until-pattern.md (wait_until helper must exist)
|
|
43
|
+
|
|
44
|
+
## Reference
|
|
45
|
+
|
|
46
|
+
See `docs/agents/ralph/plans/test-optimization-plan.md` for full analysis.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Blocking WHO/WHOIS
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Convert `who` and `whois` methods from fire-and-forget to blocking calls that return results directly. This follows the same pattern already used by `join`, `part`, and `nick` methods.
|
|
6
|
+
|
|
7
|
+
## Behavior
|
|
8
|
+
|
|
9
|
+
### WHO Command
|
|
10
|
+
|
|
11
|
+
`who(mask, timeout: DEFAULT_OPERATION_TIMEOUT)` sends WHO command and blocks until END OF WHO (315) is received.
|
|
12
|
+
|
|
13
|
+
- Returns an array of `WhoResult` objects (one per user matching the mask)
|
|
14
|
+
- Returns empty array if no matches
|
|
15
|
+
- Raises `Yaic::TimeoutError` if timeout exceeded
|
|
16
|
+
- Events (`:who`) still fire for each reply (maintains backward compatibility)
|
|
17
|
+
|
|
18
|
+
### WHOIS Command
|
|
19
|
+
|
|
20
|
+
`whois(nick, timeout: DEFAULT_OPERATION_TIMEOUT)` sends WHOIS command and blocks until END OF WHOIS (318) is received.
|
|
21
|
+
|
|
22
|
+
- Returns `WhoisResult` object if user found
|
|
23
|
+
- Returns `nil` if user not found (401 ERR_NOSUCHNICK)
|
|
24
|
+
- Raises `Yaic::TimeoutError` if timeout exceeded
|
|
25
|
+
- Event (`:whois`) still fires (maintains backward compatibility)
|
|
26
|
+
|
|
27
|
+
### WhoResult Class
|
|
28
|
+
|
|
29
|
+
Create `lib/yaic/who_result.rb` with attributes matching the `:who` event data:
|
|
30
|
+
|
|
31
|
+
- `channel` - channel name (or "*" for non-channel queries)
|
|
32
|
+
- `user` - username
|
|
33
|
+
- `host` - hostname
|
|
34
|
+
- `server` - server name
|
|
35
|
+
- `nick` - nickname
|
|
36
|
+
- `away` - boolean, true if user is away
|
|
37
|
+
- `realname` - real name
|
|
38
|
+
|
|
39
|
+
## Models
|
|
40
|
+
|
|
41
|
+
New file: `lib/yaic/who_result.rb`
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
module Yaic
|
|
45
|
+
class WhoResult
|
|
46
|
+
attr_reader :channel, :user, :host, :server, :nick, :away, :realname
|
|
47
|
+
|
|
48
|
+
def initialize(channel:, user:, host:, server:, nick:, away:, realname:)
|
|
49
|
+
# ...
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Implementation Notes
|
|
56
|
+
|
|
57
|
+
### Tracking Pending Operations
|
|
58
|
+
|
|
59
|
+
Use instance variables to track pending blocking operations:
|
|
60
|
+
|
|
61
|
+
- `@pending_who_results` - Hash of mask => array of WhoResult objects being collected
|
|
62
|
+
- `@pending_who_complete` - Hash of mask => boolean (true when 315 received)
|
|
63
|
+
|
|
64
|
+
The existing `@pending_whois` already tracks WHOIS, just need to add completion tracking.
|
|
65
|
+
|
|
66
|
+
### Handle Interleaving
|
|
67
|
+
|
|
68
|
+
Multiple WHO/WHOIS requests could be in flight. Track by mask/nick to handle interleaving correctly. The existing WHOIS implementation already handles this.
|
|
69
|
+
|
|
70
|
+
### Modify Handlers
|
|
71
|
+
|
|
72
|
+
- `handle_rpl_whoreply` (352): Create WhoResult, add to pending array
|
|
73
|
+
- `handle_rpl_endofwho` (315): Mark pending WHO complete
|
|
74
|
+
- `handle_rpl_endofwhois` (318): Already handled, add completion flag
|
|
75
|
+
|
|
76
|
+
### Waiting Pattern
|
|
77
|
+
|
|
78
|
+
Follow the existing pattern from `join`:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
def who(mask, timeout: DEFAULT_OPERATION_TIMEOUT)
|
|
82
|
+
@pending_who_results[mask] = []
|
|
83
|
+
@pending_who_complete[mask] = false
|
|
84
|
+
|
|
85
|
+
message = Message.new(command: "WHO", params: [mask])
|
|
86
|
+
@socket.write(message.to_s)
|
|
87
|
+
|
|
88
|
+
wait_until(timeout: timeout) { @pending_who_complete[mask] }
|
|
89
|
+
|
|
90
|
+
@pending_who_results.delete(mask)
|
|
91
|
+
ensure
|
|
92
|
+
@pending_who_complete.delete(mask)
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Tests
|
|
97
|
+
|
|
98
|
+
### Unit Tests
|
|
99
|
+
|
|
100
|
+
**who returns array of WhoResult objects**
|
|
101
|
+
- Given: Mock socket returns 352 replies followed by 315
|
|
102
|
+
- When: `client.who("#channel")`
|
|
103
|
+
- Then: Returns array of WhoResult with correct attributes
|
|
104
|
+
|
|
105
|
+
**who returns empty array when no matches**
|
|
106
|
+
- Given: Mock socket returns only 315 (no 352 replies)
|
|
107
|
+
- When: `client.who("nobody")`
|
|
108
|
+
- Then: Returns empty array
|
|
109
|
+
|
|
110
|
+
**who raises TimeoutError on timeout**
|
|
111
|
+
- Given: Mock socket never returns 315
|
|
112
|
+
- When: `client.who("#channel", timeout: 0.1)`
|
|
113
|
+
- Then: Raises Yaic::TimeoutError
|
|
114
|
+
|
|
115
|
+
**who still emits :who events**
|
|
116
|
+
- Given: Mock socket returns 352 replies
|
|
117
|
+
- When: `client.who("#channel")` with event listener attached
|
|
118
|
+
- Then: Events fire AND method returns results
|
|
119
|
+
|
|
120
|
+
**whois returns WhoisResult**
|
|
121
|
+
- Given: Mock socket returns WHOIS numerics followed by 318
|
|
122
|
+
- When: `client.whois("nick")`
|
|
123
|
+
- Then: Returns WhoisResult with correct attributes
|
|
124
|
+
|
|
125
|
+
**whois returns nil for unknown nick**
|
|
126
|
+
- Given: Mock socket returns 401 ERR_NOSUCHNICK then 318
|
|
127
|
+
- When: `client.whois("nobody")`
|
|
128
|
+
- Then: Returns nil
|
|
129
|
+
|
|
130
|
+
**whois raises TimeoutError on timeout**
|
|
131
|
+
- Given: Mock socket never returns 318
|
|
132
|
+
- When: `client.whois("nick", timeout: 0.1)`
|
|
133
|
+
- Then: Raises Yaic::TimeoutError
|
|
134
|
+
|
|
135
|
+
### Integration Tests
|
|
136
|
+
|
|
137
|
+
**who channel returns all users**
|
|
138
|
+
- Given: Two clients connected to same channel
|
|
139
|
+
- When: `client1.who("#channel")`
|
|
140
|
+
- Then: Returns array with both users' info
|
|
141
|
+
|
|
142
|
+
**who nick returns single user**
|
|
143
|
+
- Given: Two clients connected
|
|
144
|
+
- When: `client1.who(client2_nick)`
|
|
145
|
+
- Then: Returns array with one WhoResult for client2
|
|
146
|
+
|
|
147
|
+
**whois returns user info**
|
|
148
|
+
- Given: Two clients connected
|
|
149
|
+
- When: `client1.whois(client2_nick)`
|
|
150
|
+
- Then: Returns WhoisResult with nick, user, host, realname
|
|
151
|
+
|
|
152
|
+
**whois unknown returns nil**
|
|
153
|
+
- Given: Client connected
|
|
154
|
+
- When: `client.whois("nobody_exists")`
|
|
155
|
+
- Then: Returns nil (not raises)
|
|
156
|
+
|
|
157
|
+
## Dependencies
|
|
158
|
+
|
|
159
|
+
None - builds on existing WHO/WHOIS handling from 14-who-whois.md
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Verbose Mode
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Add a `verbose:` option to the Client constructor that outputs debug information about connection state changes and blocking operations. Helps developers understand what the client is doing.
|
|
6
|
+
|
|
7
|
+
## Behavior
|
|
8
|
+
|
|
9
|
+
### Enabling Verbose Mode
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
client = Yaic::Client.new(
|
|
13
|
+
server: "irc.example.com",
|
|
14
|
+
nick: "mynick",
|
|
15
|
+
verbose: true # defaults to false
|
|
16
|
+
)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### What Gets Logged
|
|
20
|
+
|
|
21
|
+
**Connection State Changes**
|
|
22
|
+
- When connecting starts
|
|
23
|
+
- When connected successfully
|
|
24
|
+
- When disconnected (including reason if available)
|
|
25
|
+
- When connection state changes (registering, etc.)
|
|
26
|
+
|
|
27
|
+
**Blocking Operations**
|
|
28
|
+
- When waiting for an operation to complete (join, part, nick, who, whois)
|
|
29
|
+
- When the wait completes
|
|
30
|
+
|
|
31
|
+
### Output Format
|
|
32
|
+
|
|
33
|
+
Simple, readable format to STDERR:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
[YAIC] Connecting to irc.example.com:6697 (SSL)...
|
|
37
|
+
[YAIC] Connected, registering...
|
|
38
|
+
[YAIC] Registration complete
|
|
39
|
+
[YAIC] Joining #channel...
|
|
40
|
+
[YAIC] Joined #channel
|
|
41
|
+
[YAIC] Sending WHO #channel...
|
|
42
|
+
[YAIC] WHO complete (3 results)
|
|
43
|
+
[YAIC] Sending WHOIS nick...
|
|
44
|
+
[YAIC] WHOIS complete
|
|
45
|
+
[YAIC] Disconnected
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Output Destination
|
|
49
|
+
|
|
50
|
+
Use `$stderr.puts` for output. This keeps it separate from application output and works well with logging redirection.
|
|
51
|
+
|
|
52
|
+
## Implementation Notes
|
|
53
|
+
|
|
54
|
+
### Add verbose attribute
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
def initialize(server:, port: 6697, nick:, user: nil, realname: nil, ssl: true, verbose: false)
|
|
58
|
+
@verbose = verbose
|
|
59
|
+
# ...
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Add logging helper
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def log(message)
|
|
69
|
+
return unless @verbose
|
|
70
|
+
$stderr.puts "[YAIC] #{message}"
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Add logging calls
|
|
75
|
+
|
|
76
|
+
In `connect`:
|
|
77
|
+
```ruby
|
|
78
|
+
log "Connecting to #{@server}:#{@port}#{@ssl ? ' (SSL)' : ''}..."
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
After registration complete (in handler for 001):
|
|
82
|
+
```ruby
|
|
83
|
+
log "Connected"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
In `set_state`:
|
|
87
|
+
```ruby
|
|
88
|
+
log "State: #{state}" # or more friendly messages per state
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
In `join`:
|
|
92
|
+
```ruby
|
|
93
|
+
log "Joining #{channel}..."
|
|
94
|
+
# after wait_until
|
|
95
|
+
log "Joined #{channel}"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
In `part`:
|
|
99
|
+
```ruby
|
|
100
|
+
log "Parting #{channel}..."
|
|
101
|
+
log "Parted #{channel}"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
In `who`:
|
|
105
|
+
```ruby
|
|
106
|
+
log "Sending WHO #{mask}..."
|
|
107
|
+
# after wait_until
|
|
108
|
+
log "WHO complete (#{results.size} results)"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
In `whois`:
|
|
112
|
+
```ruby
|
|
113
|
+
log "Sending WHOIS #{nick}..."
|
|
114
|
+
# after wait_until
|
|
115
|
+
log "WHOIS complete"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
In disconnect handler:
|
|
119
|
+
```ruby
|
|
120
|
+
log "Disconnected"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Tests
|
|
124
|
+
|
|
125
|
+
### Unit Tests
|
|
126
|
+
|
|
127
|
+
**verbose false produces no output**
|
|
128
|
+
- Given: Client created with `verbose: false` (default)
|
|
129
|
+
- When: Client connects and performs operations
|
|
130
|
+
- Then: No output to stderr
|
|
131
|
+
|
|
132
|
+
**verbose true logs connection**
|
|
133
|
+
- Given: Client created with `verbose: true`
|
|
134
|
+
- When: Client connects
|
|
135
|
+
- Then: Stderr contains "[YAIC] Connecting to" message
|
|
136
|
+
|
|
137
|
+
**verbose true logs state changes**
|
|
138
|
+
- Given: Client created with `verbose: true`
|
|
139
|
+
- When: Client connects successfully
|
|
140
|
+
- Then: Stderr contains "[YAIC] Connected" message
|
|
141
|
+
|
|
142
|
+
**verbose true logs join**
|
|
143
|
+
- Given: Connected client with `verbose: true`
|
|
144
|
+
- When: `client.join("#channel")`
|
|
145
|
+
- Then: Stderr contains "[YAIC] Joining #channel..." and "[YAIC] Joined #channel"
|
|
146
|
+
|
|
147
|
+
**verbose true logs who**
|
|
148
|
+
- Given: Connected client with `verbose: true`
|
|
149
|
+
- When: `client.who("#channel")`
|
|
150
|
+
- Then: Stderr contains "[YAIC] Sending WHO" and "[YAIC] WHO complete"
|
|
151
|
+
|
|
152
|
+
**verbose true logs whois**
|
|
153
|
+
- Given: Connected client with `verbose: true`
|
|
154
|
+
- When: `client.whois("nick")`
|
|
155
|
+
- Then: Stderr contains "[YAIC] Sending WHOIS" and "[YAIC] WHOIS complete"
|
|
156
|
+
|
|
157
|
+
### Integration Tests
|
|
158
|
+
|
|
159
|
+
**verbose mode produces expected output sequence**
|
|
160
|
+
- Given: Client with `verbose: true`
|
|
161
|
+
- When: Connect, join channel, who channel, disconnect
|
|
162
|
+
- Then: Stderr output matches expected sequence of log messages
|
|
163
|
+
|
|
164
|
+
## Dependencies
|
|
165
|
+
|
|
166
|
+
- 24-blocking-who-whois.md (for WHO/WHOIS logging)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Test Optimization Plan
|
|
2
|
+
|
|
3
|
+
## Profiling Results
|
|
4
|
+
|
|
5
|
+
**Total test time**: 173 seconds
|
|
6
|
+
**Total tests**: 267
|
|
7
|
+
**Tests >= 1 second**: 79 tests (172.51s cumulative)
|
|
8
|
+
**Tests < 1 second**: 188 tests (0.54s cumulative)
|
|
9
|
+
|
|
10
|
+
### Slowest Tests (> 3 seconds)
|
|
11
|
+
|
|
12
|
+
| Test | Time | Root Cause |
|
|
13
|
+
|------|------|------------|
|
|
14
|
+
| `PingPongIntegrationTest#test_client_automatically_responds_to_server_ping` | 7.01s | Intentional `sleep 6` to wait for server PING |
|
|
15
|
+
| `NamesIntegrationTest#test_multi_message_names` | 5.73s | Creates 5 clients, each taking ~1s to connect |
|
|
16
|
+
| `SocketIntegrationTest#test_ssl_read_write` | 5.01s | `read_multiple(socket, 5)` - full timeout wait |
|
|
17
|
+
| `RegistrationIntegrationTest#test_invalid_nickname` | 5.01s | `read_multiple(socket, 5)` - full timeout wait |
|
|
18
|
+
| `SocketIntegrationTest#test_write_message` | 5.00s | `read_multiple(socket, 5)` - full timeout wait |
|
|
19
|
+
| `RegistrationIntegrationTest#test_empty_nickname` | 5.00s | `read_multiple(socket, 5)` - full timeout wait |
|
|
20
|
+
| `KickIntegrationTest#test_receive_kick_others` | 4.13s | Creates 3 clients + oper setup + sleep 0.5 |
|
|
21
|
+
| `ModeIntegrationTest#test_give_op_to_user` | 3.12s | 2 clients + oper setup + sleep 0.5 |
|
|
22
|
+
| `KickIntegrationTest#test_receive_kick_self` | 3.12s | 2 clients + oper setup + sleep 0.5 |
|
|
23
|
+
| `KickIntegrationTest#test_kick_user` | 3.12s | 2 clients + oper setup + sleep 0.5 |
|
|
24
|
+
| `TopicIntegrationTest#test_receive_topic_change` | 3.06s | 2 clients + sleep 0.5 |
|
|
25
|
+
| `PingPongIntegrationTest#test_respond_to_server_ping_during_registration` | 3.02s | `read_multiple(socket, 3)` timeout |
|
|
26
|
+
|
|
27
|
+
### Root Causes Identified
|
|
28
|
+
|
|
29
|
+
1. **Fixed sleep statements**: Many tests use `sleep 0.5` to wait for IRC responses
|
|
30
|
+
2. **Full timeout waits**: `read_multiple(socket, N)` always waits N seconds even if data arrives immediately
|
|
31
|
+
3. **Multiple client connections**: Each `client.connect` takes ~1 second due to registration
|
|
32
|
+
4. **Sequential test execution**: Tests run one at a time despite being independent
|
|
33
|
+
|
|
34
|
+
## Proposed Optimizations
|
|
35
|
+
|
|
36
|
+
### Optimization 1: Enable Parallel Test Execution
|
|
37
|
+
|
|
38
|
+
**Description**: Use minitest's built-in parallel executor to run tests concurrently.
|
|
39
|
+
|
|
40
|
+
**Changes**:
|
|
41
|
+
- Add `parallelize_me!` to test classes or enable globally
|
|
42
|
+
- Tests already use unique nicks/channels via `Process.pid` and `Time.now.to_i`
|
|
43
|
+
|
|
44
|
+
**Expected Impact**: 3-4x speedup (utilize multiple CPU cores)
|
|
45
|
+
|
|
46
|
+
**Risk**: Low - tests already use unique identifiers, no shared state between tests
|
|
47
|
+
|
|
48
|
+
**Implementation**:
|
|
49
|
+
```ruby
|
|
50
|
+
# test/test_helper.rb
|
|
51
|
+
require "minitest/autorun"
|
|
52
|
+
class Minitest::Test
|
|
53
|
+
parallelize_me!
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Optimization 2: Replace `sleep` with `wait_until` Pattern
|
|
58
|
+
|
|
59
|
+
**Description**: Replace `sleep 0.5` with `wait_until` style helpers that return as soon as condition is met.
|
|
60
|
+
|
|
61
|
+
**Changes**:
|
|
62
|
+
- Create `wait_until(timeout: 2) { condition }` helper in test_helper.rb
|
|
63
|
+
- Replace `sleep 0.5` with targeted `wait_until` calls
|
|
64
|
+
|
|
65
|
+
**Expected Impact**: Each test saves 0.3-0.4s (event typically arrives in 0.1s)
|
|
66
|
+
|
|
67
|
+
**Risk**: Low - uses existing event system, fails fast if event doesn't arrive
|
|
68
|
+
|
|
69
|
+
**Example**:
|
|
70
|
+
```ruby
|
|
71
|
+
# Before
|
|
72
|
+
client1.kick(@test_channel, @test_nick2)
|
|
73
|
+
sleep 0.5
|
|
74
|
+
assert kick_received
|
|
75
|
+
|
|
76
|
+
# After
|
|
77
|
+
client1.kick(@test_channel, @test_nick2)
|
|
78
|
+
wait_until { kick_received }
|
|
79
|
+
assert kick_received
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Optimization 3: Replace `read_multiple`/`read_until_*` with `wait_until` Pattern
|
|
83
|
+
|
|
84
|
+
**Description**: Rename and refactor `read_multiple`, `read_until_pong`, etc. to use consistent `wait_until` naming.
|
|
85
|
+
|
|
86
|
+
**Changes**:
|
|
87
|
+
- Replace `read_multiple(socket, 5)` with `wait_until(timeout: 2) { messages.any? { |m| m.command == "432" } }`
|
|
88
|
+
- Remove confusingly-named helpers in favor of inline `wait_until` blocks
|
|
89
|
+
|
|
90
|
+
**Expected Impact**: 4 tests save ~15 seconds total
|
|
91
|
+
|
|
92
|
+
**Risk**: Low - clearer code, faster execution
|
|
93
|
+
|
|
94
|
+
**Example**:
|
|
95
|
+
```ruby
|
|
96
|
+
# Before (always waits 5 seconds)
|
|
97
|
+
messages = read_multiple(socket, 5)
|
|
98
|
+
erroneous = messages.find { |m| m.command == "432" }
|
|
99
|
+
|
|
100
|
+
# After (returns when error found or timeout)
|
|
101
|
+
erroneous = nil
|
|
102
|
+
wait_until(timeout: 2) do
|
|
103
|
+
raw = socket.read
|
|
104
|
+
if raw
|
|
105
|
+
msg = Yaic::Message.parse(raw)
|
|
106
|
+
erroneous = msg if msg&.command == "432"
|
|
107
|
+
end
|
|
108
|
+
erroneous
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Optimization 4: Optimize Ping Test with Faster Server Config
|
|
113
|
+
|
|
114
|
+
**Description**: The `test_client_automatically_responds_to_server_ping` waits 6 seconds for server PING.
|
|
115
|
+
|
|
116
|
+
**Changes**:
|
|
117
|
+
- Reduce server's ping frequency in test config from 5s to 3s
|
|
118
|
+
- Use `wait_until` to detect when PING/PONG cycle completes instead of fixed sleep
|
|
119
|
+
|
|
120
|
+
**Expected Impact**: Save ~4 seconds
|
|
121
|
+
|
|
122
|
+
**Risk**: Low - just reducing wait time and using event-driven approach
|
|
123
|
+
|
|
124
|
+
## Implementation Order
|
|
125
|
+
|
|
126
|
+
1. **Parallel execution** (biggest impact, lowest risk)
|
|
127
|
+
2. **Replace `sleep 0.5` with event waits** (many small wins)
|
|
128
|
+
3. **Fix `read_multiple` pattern** (4 tests)
|
|
129
|
+
4. **Optimize ping test** (1 test)
|
|
130
|
+
|
|
131
|
+
## Parallelization Safety Analysis
|
|
132
|
+
|
|
133
|
+
### Already Safe (unique identifiers per test)
|
|
134
|
+
|
|
135
|
+
All integration tests generate unique identifiers:
|
|
136
|
+
```ruby
|
|
137
|
+
@test_nick = "t#{Process.pid}#{Time.now.to_i % 10000}"
|
|
138
|
+
@test_channel = "#test#{Process.pid}#{Time.now.to_i % 10000}"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
When running in parallel, each test process/thread has different:
|
|
142
|
+
- `Process.pid` (if forked)
|
|
143
|
+
- `Time.now.to_i % 10000` (high probability of uniqueness)
|
|
144
|
+
|
|
145
|
+
### Potential Issue: Thread-based parallelism
|
|
146
|
+
|
|
147
|
+
If using threads (not forks), `Process.pid` will be the same for all tests. Need to add thread ID:
|
|
148
|
+
```ruby
|
|
149
|
+
@test_nick = "t#{Process.pid}_#{Thread.current.object_id % 10000}_#{Time.now.to_i % 10000}"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Shared Resources
|
|
153
|
+
|
|
154
|
+
- IRC server: Can handle multiple concurrent connections
|
|
155
|
+
- `become_oper`: Uses single shared oper account - safe, oper can be used from multiple connections
|
|
156
|
+
- No file system or database shared state
|
|
157
|
+
|
|
158
|
+
## Verification Plan
|
|
159
|
+
|
|
160
|
+
1. Run `rake test` and verify all tests pass
|
|
161
|
+
2. Count tests: should be 267
|
|
162
|
+
3. Count assertions: should be 575
|
|
163
|
+
4. Time should be significantly reduced (target: < 60s)
|
|
164
|
+
|
|
165
|
+
## Risk Assessment
|
|
166
|
+
|
|
167
|
+
| Change | Risk | Mitigation |
|
|
168
|
+
|--------|------|------------|
|
|
169
|
+
| Parallel execution | Low | Tests already isolated by design |
|
|
170
|
+
| Event-driven waits | Low | Fallback timeout prevents hangs |
|
|
171
|
+
| read_multiple fix | Low | Backwards compatible |
|
|
172
|
+
| Ping test optimization | Medium | May need server config tuning |
|