zone 0.1.1 → 0.1.2
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 +77 -0
- data/README.md +122 -27
- data/TEST_PLAN.md +911 -0
- data/completions/README.md +126 -0
- data/completions/_zone +89 -0
- data/docs/user-experience-review.md +150 -0
- data/exe/zone +2 -265
- data/lib/zone/cli.rb +64 -0
- data/lib/zone/colors.rb +179 -0
- data/lib/zone/field.rb +67 -0
- data/lib/zone/field_line.rb +97 -0
- data/lib/zone/field_mapping.rb +51 -0
- data/lib/zone/input.rb +52 -0
- data/lib/zone/logging.rb +38 -0
- data/lib/zone/options.rb +142 -0
- data/lib/zone/output.rb +39 -0
- data/lib/zone/pattern.rb +59 -0
- data/lib/zone/timestamp.rb +138 -0
- data/lib/zone/timestamp_patterns.rb +169 -0
- data/lib/zone/transform.rb +69 -0
- data/lib/zone/version.rb +1 -1
- data/lib/zone.rb +45 -1
- data/todo.md +85 -0
- metadata +19 -1
data/TEST_PLAN.md
ADDED
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
# Zone CLI - Comprehensive Test Plan
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Based on Minitest documentation (Rule 1), this plan covers:
|
|
6
|
+
1. **Unit tests** for each domain class
|
|
7
|
+
2. **Integration tests** for CLI command execution
|
|
8
|
+
3. **All assertion types** appropriate for each test
|
|
9
|
+
|
|
10
|
+
## Minitest Assertions Available
|
|
11
|
+
|
|
12
|
+
From `ri Minitest::Assertions`:
|
|
13
|
+
|
|
14
|
+
### **Equality & Comparison**
|
|
15
|
+
- `assert_equal(exp, act)` - Fails unless exp == act
|
|
16
|
+
- `refute_equal(exp, act)` - Fails if exp == act
|
|
17
|
+
- `assert_same(exp, act)` - Fails unless exp.equal?(act)
|
|
18
|
+
- `assert_nil(obj)` - Fails unless obj is nil
|
|
19
|
+
- `refute_nil(obj)` - Fails if obj is nil
|
|
20
|
+
|
|
21
|
+
### **Type Checking**
|
|
22
|
+
- `assert_instance_of(cls, obj)` - Fails unless obj is an instance of cls
|
|
23
|
+
- `assert_kind_of(cls, obj)` - Fails unless obj is a kind of cls
|
|
24
|
+
- `assert_respond_to(obj, meth)` - Fails unless obj responds to meth
|
|
25
|
+
|
|
26
|
+
### **Collections**
|
|
27
|
+
- `assert_includes(collection, obj)` - Fails unless collection includes obj
|
|
28
|
+
- `assert_empty(obj)` - Fails unless obj is empty
|
|
29
|
+
- `refute_empty(obj)` - Fails if obj is empty
|
|
30
|
+
|
|
31
|
+
### **Pattern Matching**
|
|
32
|
+
- `assert_match(matcher, obj)` - Fails unless matcher =~ obj
|
|
33
|
+
- `refute_match(matcher, obj)` - Fails if matcher =~ obj
|
|
34
|
+
|
|
35
|
+
### **Exceptions**
|
|
36
|
+
- `assert_raises(*exp) { }` - Fails unless block raises one of exp
|
|
37
|
+
- `assert_silent { }` - Fails if block outputs to stdout or stderr
|
|
38
|
+
|
|
39
|
+
### **IO Capture**
|
|
40
|
+
- `capture_io { }` - Captures $stdout and $stderr (for in-process)
|
|
41
|
+
- `capture_subprocess_io { }` - Captures subprocess IO (for shell commands)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Test File Structure
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
test/
|
|
49
|
+
test_helper.rb # Common test setup
|
|
50
|
+
zone/
|
|
51
|
+
test_timestamp.rb # Zone::Timestamp unit tests
|
|
52
|
+
test_field_mapping.rb # Zone::FieldMapping unit tests
|
|
53
|
+
test_field_line.rb # Zone::FieldLine unit tests
|
|
54
|
+
test_zone_module.rb # Zone.find module method tests
|
|
55
|
+
integration/
|
|
56
|
+
test_cli_integration.rb # End-to-end CLI tests
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 1. test/test_helper.rb
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
# frozen_string_literal: true
|
|
65
|
+
|
|
66
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
67
|
+
require "zone"
|
|
68
|
+
require "minitest/autorun"
|
|
69
|
+
|
|
70
|
+
# Make diffs prettier
|
|
71
|
+
Minitest::Test.make_my_diffs_pretty!
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 2. test/zone/test_timestamp.rb
|
|
77
|
+
|
|
78
|
+
### **Purpose**: Test Zone::Timestamp parsing, conversion, and formatting
|
|
79
|
+
|
|
80
|
+
### **Assertions to Use**:
|
|
81
|
+
- `assert_instance_of` - Verify return type
|
|
82
|
+
- `assert_equal` - Compare formatted output
|
|
83
|
+
- `assert_raises` - Test error cases
|
|
84
|
+
- `assert_respond_to` - Verify API methods exist
|
|
85
|
+
- `assert_match` - Verify output format patterns
|
|
86
|
+
|
|
87
|
+
### **Test Cases**:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
# frozen_string_literal: true
|
|
91
|
+
|
|
92
|
+
require "test_helper"
|
|
93
|
+
|
|
94
|
+
class TestTimestamp < Minitest::Test
|
|
95
|
+
def test_parse_time_object
|
|
96
|
+
time = Time.now
|
|
97
|
+
timestamp = Zone::Timestamp.parse(time)
|
|
98
|
+
|
|
99
|
+
assert_instance_of Zone::Timestamp, timestamp
|
|
100
|
+
assert_equal time, timestamp.time
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test_parse_datetime_object
|
|
104
|
+
datetime = DateTime.now
|
|
105
|
+
timestamp = Zone::Timestamp.parse(datetime)
|
|
106
|
+
|
|
107
|
+
assert_instance_of Zone::Timestamp, timestamp
|
|
108
|
+
assert_instance_of Time, timestamp.time
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def test_parse_date_object
|
|
112
|
+
date = Date.today
|
|
113
|
+
timestamp = Zone::Timestamp.parse(date)
|
|
114
|
+
|
|
115
|
+
assert_instance_of Zone::Timestamp, timestamp
|
|
116
|
+
assert_instance_of Time, timestamp.time
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_parse_unix_timestamp_seconds
|
|
120
|
+
# Unix epoch for 2025-01-15 10:30:00 UTC
|
|
121
|
+
timestamp = Zone::Timestamp.parse("1736937000")
|
|
122
|
+
|
|
123
|
+
assert_equal 1736937000, timestamp.time.to_i
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_parse_unix_timestamp_milliseconds
|
|
127
|
+
# 13 digits - milliseconds precision
|
|
128
|
+
timestamp = Zone::Timestamp.parse("1736937000000")
|
|
129
|
+
|
|
130
|
+
assert_equal 1736937000, timestamp.time.to_i
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test_parse_iso8601_string
|
|
134
|
+
timestamp = Zone::Timestamp.parse("2025-01-15T10:30:00Z")
|
|
135
|
+
|
|
136
|
+
assert_equal 2025, timestamp.time.year
|
|
137
|
+
assert_equal 1, timestamp.time.month
|
|
138
|
+
assert_equal 15, timestamp.time.day
|
|
139
|
+
assert_equal 10, timestamp.time.hour
|
|
140
|
+
assert_equal 30, timestamp.time.min
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def test_parse_relative_time_ago
|
|
144
|
+
timestamp = Zone::Timestamp.parse("5 minutes ago")
|
|
145
|
+
diff = Time.now - timestamp.time
|
|
146
|
+
|
|
147
|
+
assert_in_delta 300, diff, 2 # Within 2 seconds of 5 minutes
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_parse_relative_time_from_now
|
|
151
|
+
timestamp = Zone::Timestamp.parse("1 hour from now")
|
|
152
|
+
diff = timestamp.time - Time.now
|
|
153
|
+
|
|
154
|
+
assert_in_delta 3600, diff, 2 # Within 2 seconds of 1 hour
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def test_parse_invalid_input_raises_error
|
|
158
|
+
error = assert_raises(ArgumentError) do
|
|
159
|
+
Zone::Timestamp.parse("not a valid timestamp")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
assert_match(/Could not parse time/, error.message)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def test_in_zone_returns_new_timestamp
|
|
166
|
+
original = Zone::Timestamp.parse("2025-01-15T10:30:00Z")
|
|
167
|
+
tokyo = original.in_zone("Tokyo")
|
|
168
|
+
|
|
169
|
+
assert_instance_of Zone::Timestamp, tokyo
|
|
170
|
+
refute_same original, tokyo
|
|
171
|
+
assert_equal "Tokyo", tokyo.zone
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def test_in_utc_returns_new_timestamp
|
|
175
|
+
local = Zone::Timestamp.parse(Time.now.to_s)
|
|
176
|
+
utc = local.in_utc
|
|
177
|
+
|
|
178
|
+
assert_instance_of Zone::Timestamp, utc
|
|
179
|
+
assert_equal "UTC", utc.zone
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def test_in_local_returns_new_timestamp
|
|
183
|
+
utc = Zone::Timestamp.parse("2025-01-15T10:30:00Z")
|
|
184
|
+
local = utc.in_local
|
|
185
|
+
|
|
186
|
+
assert_instance_of Zone::Timestamp, local
|
|
187
|
+
assert_equal "local", local.zone
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def test_to_iso8601_format
|
|
191
|
+
timestamp = Zone::Timestamp.parse("2025-01-15T10:30:00Z")
|
|
192
|
+
formatted = timestamp.to_iso8601
|
|
193
|
+
|
|
194
|
+
assert_match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, formatted)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def test_to_unix_returns_integer
|
|
198
|
+
timestamp = Zone::Timestamp.parse("2025-01-15T10:30:00Z")
|
|
199
|
+
unix = timestamp.to_unix
|
|
200
|
+
|
|
201
|
+
assert_instance_of Integer, unix
|
|
202
|
+
assert_equal 1736937000, unix
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def test_to_pretty_format
|
|
206
|
+
timestamp = Zone::Timestamp.parse("2025-01-15T10:30:00Z")
|
|
207
|
+
pretty = timestamp.to_pretty
|
|
208
|
+
|
|
209
|
+
assert_match(/Jan \d+/, pretty)
|
|
210
|
+
assert_match(/\d{1,2}:\d{2} [AP]M/, pretty)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def test_strftime_custom_format
|
|
214
|
+
timestamp = Zone::Timestamp.parse("2025-01-15T10:30:00Z")
|
|
215
|
+
formatted = timestamp.strftime("%Y-%m-%d")
|
|
216
|
+
|
|
217
|
+
assert_equal "2025-01-15", formatted
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def test_chainable_operations
|
|
221
|
+
result = Zone::Timestamp
|
|
222
|
+
.parse("2025-01-15T10:30:00Z")
|
|
223
|
+
.in_zone("Tokyo")
|
|
224
|
+
.to_iso8601
|
|
225
|
+
|
|
226
|
+
assert_instance_of String, result
|
|
227
|
+
assert_match(/\+09:00/, result) # Tokyo timezone offset
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def test_responds_to_all_public_methods
|
|
231
|
+
timestamp = Zone::Timestamp.parse("2025-01-15T10:30:00Z")
|
|
232
|
+
|
|
233
|
+
assert_respond_to timestamp, :in_zone
|
|
234
|
+
assert_respond_to timestamp, :in_utc
|
|
235
|
+
assert_respond_to timestamp, :in_local
|
|
236
|
+
assert_respond_to timestamp, :to_iso8601
|
|
237
|
+
assert_respond_to timestamp, :to_unix
|
|
238
|
+
assert_respond_to timestamp, :to_pretty
|
|
239
|
+
assert_respond_to timestamp, :strftime
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## 3. test/zone/test_field_mapping.rb
|
|
247
|
+
|
|
248
|
+
### **Purpose**: Test Zone::FieldMapping index resolution
|
|
249
|
+
|
|
250
|
+
### **Assertions to Use**:
|
|
251
|
+
- `assert_equal` - Verify index resolution
|
|
252
|
+
- `assert_raises` - Test error cases with invalid fields
|
|
253
|
+
- `assert_includes` - Check names array
|
|
254
|
+
- `assert_empty` - Verify numeric-only mappings
|
|
255
|
+
|
|
256
|
+
### **Test Cases**:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
# frozen_string_literal: true
|
|
260
|
+
|
|
261
|
+
require "test_helper"
|
|
262
|
+
|
|
263
|
+
class TestFieldMapping < Minitest::Test
|
|
264
|
+
def test_from_fields_creates_mapping
|
|
265
|
+
fields = ["name", "age", "city"]
|
|
266
|
+
mapping = Zone::FieldMapping.from_fields(fields)
|
|
267
|
+
|
|
268
|
+
assert_instance_of Zone::FieldMapping, mapping
|
|
269
|
+
assert_equal 0, mapping["name"]
|
|
270
|
+
assert_equal 1, mapping["age"]
|
|
271
|
+
assert_equal 2, mapping["city"]
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def test_numeric_creates_numeric_only_mapping
|
|
275
|
+
mapping = Zone::FieldMapping.numeric
|
|
276
|
+
|
|
277
|
+
assert_instance_of Zone::FieldMapping, mapping
|
|
278
|
+
refute mapping.has_names?
|
|
279
|
+
assert_empty mapping.names
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def test_resolve_with_field_name
|
|
283
|
+
mapping = Zone::FieldMapping.from_fields(["name", "timestamp", "value"])
|
|
284
|
+
|
|
285
|
+
assert_equal 1, mapping.resolve("timestamp")
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def test_resolve_with_integer_converts_to_zero_based
|
|
289
|
+
mapping = Zone::FieldMapping.numeric
|
|
290
|
+
|
|
291
|
+
assert_equal 0, mapping.resolve(1) # 1-based to 0-based
|
|
292
|
+
assert_equal 1, mapping.resolve(2)
|
|
293
|
+
assert_equal 9, mapping.resolve(10)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def test_resolve_with_numeric_string
|
|
297
|
+
mapping = Zone::FieldMapping.numeric
|
|
298
|
+
|
|
299
|
+
assert_equal 0, mapping["1"] # String "1" -> 0
|
|
300
|
+
assert_equal 1, mapping["2"]
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def test_bracket_operator_alias
|
|
304
|
+
mapping = Zone::FieldMapping.from_fields(["a", "b", "c"])
|
|
305
|
+
|
|
306
|
+
assert_equal 0, mapping["a"]
|
|
307
|
+
assert_equal 1, mapping[2]
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def test_raises_error_for_missing_field_name
|
|
311
|
+
mapping = Zone::FieldMapping.from_fields(["a", "b"])
|
|
312
|
+
|
|
313
|
+
error = assert_raises(KeyError) do
|
|
314
|
+
mapping.resolve("nonexistent")
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
assert_match(/not found/, error.message)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def test_raises_error_for_invalid_key_type
|
|
321
|
+
mapping = Zone::FieldMapping.numeric
|
|
322
|
+
|
|
323
|
+
assert_raises(ArgumentError) do
|
|
324
|
+
mapping.resolve(3.14) # Float is not valid
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def test_names_returns_field_names
|
|
329
|
+
fields = ["foo", "bar", "baz"]
|
|
330
|
+
mapping = Zone::FieldMapping.from_fields(fields)
|
|
331
|
+
|
|
332
|
+
assert_equal fields, mapping.names
|
|
333
|
+
assert_includes mapping.names, "foo"
|
|
334
|
+
assert_includes mapping.names, "bar"
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def test_names_empty_for_numeric_mapping
|
|
338
|
+
mapping = Zone::FieldMapping.numeric
|
|
339
|
+
|
|
340
|
+
assert_empty mapping.names
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def test_has_names_predicate
|
|
344
|
+
with_names = Zone::FieldMapping.from_fields(["a"])
|
|
345
|
+
numeric = Zone::FieldMapping.numeric
|
|
346
|
+
|
|
347
|
+
assert with_names.has_names?
|
|
348
|
+
refute numeric.has_names?
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## 4. test/zone/test_field_line.rb
|
|
356
|
+
|
|
357
|
+
### **Purpose**: Test Zone::FieldLine parsing and transformation
|
|
358
|
+
|
|
359
|
+
### **Assertions to Use**:
|
|
360
|
+
- `assert_equal` - Compare parsed fields and output
|
|
361
|
+
- `assert_instance_of` - Verify return types
|
|
362
|
+
- `assert_same` - Verify transform returns self (for chaining)
|
|
363
|
+
- `assert_match` - Verify delimiter inference
|
|
364
|
+
|
|
365
|
+
### **Test Cases**:
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
# frozen_string_literal: true
|
|
369
|
+
|
|
370
|
+
require "test_helper"
|
|
371
|
+
|
|
372
|
+
class TestFieldLine < Minitest::Test
|
|
373
|
+
def test_parse_simple_comma_delimited
|
|
374
|
+
line = Zone::FieldLine.parse("foo,bar,baz")
|
|
375
|
+
|
|
376
|
+
assert_instance_of Zone::FieldLine, line
|
|
377
|
+
assert_equal ["foo", "bar", "baz"], line.fields
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def test_parse_tab_delimited
|
|
381
|
+
line = Zone::FieldLine.parse("foo\tbar\tbaz")
|
|
382
|
+
|
|
383
|
+
assert_equal ["foo", "bar", "baz"], line.fields
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def test_parse_whitespace_delimited
|
|
387
|
+
line = Zone::FieldLine.parse("foo bar baz")
|
|
388
|
+
|
|
389
|
+
assert_equal ["foo", "bar", "baz"], line.fields
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def test_parse_with_explicit_delimiter
|
|
393
|
+
line = Zone::FieldLine.parse(
|
|
394
|
+
"foo|bar|baz",
|
|
395
|
+
delimiter: "|"
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
assert_equal ["foo", "bar", "baz"], line.fields
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def test_parse_with_mapping
|
|
402
|
+
mapping = Zone::FieldMapping.from_fields(["name", "value"])
|
|
403
|
+
line = Zone::FieldLine.parse(
|
|
404
|
+
"test,100",
|
|
405
|
+
mapping: mapping
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
assert_equal "test", line["name"]
|
|
409
|
+
assert_equal "100", line["value"]
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def test_bracket_access_by_index
|
|
413
|
+
line = Zone::FieldLine.parse("a,b,c")
|
|
414
|
+
|
|
415
|
+
assert_equal "a", line[1] # 1-based
|
|
416
|
+
assert_equal "b", line[2]
|
|
417
|
+
assert_equal "c", line[3]
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def test_bracket_access_by_name
|
|
421
|
+
mapping = Zone::FieldMapping.from_fields(["x", "y"])
|
|
422
|
+
line = Zone::FieldLine.parse(
|
|
423
|
+
"10,20",
|
|
424
|
+
mapping: mapping
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
assert_equal "10", line["x"]
|
|
428
|
+
assert_equal "20", line["y"]
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def test_transform_by_index
|
|
432
|
+
line = Zone::FieldLine.parse("foo,bar,baz")
|
|
433
|
+
result = line.transform(2) { |v| v.upcase }
|
|
434
|
+
|
|
435
|
+
assert_same line, result # Returns self for chaining
|
|
436
|
+
assert_equal "foo,BAR,baz", line.to_s
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def test_transform_by_name
|
|
440
|
+
mapping = Zone::FieldMapping.from_fields(["name", "value"])
|
|
441
|
+
line = Zone::FieldLine.parse(
|
|
442
|
+
"test,100",
|
|
443
|
+
mapping: mapping
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
line.transform("value") { |v| v.to_i * 2 }
|
|
447
|
+
|
|
448
|
+
assert_equal "test,200", line.to_s
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def test_transform_all_fields
|
|
452
|
+
line = Zone::FieldLine.parse("a,b,c")
|
|
453
|
+
line.transform_all(&:upcase)
|
|
454
|
+
|
|
455
|
+
assert_equal "A,B,C", line.to_s
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def test_to_s_reconstructs_line
|
|
459
|
+
line = Zone::FieldLine.parse("foo,bar,baz")
|
|
460
|
+
|
|
461
|
+
assert_equal "foo,bar,baz", line.to_s
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def test_to_s_single_field
|
|
465
|
+
line = Zone::FieldLine.parse("2025-01-15T10:30:00Z")
|
|
466
|
+
|
|
467
|
+
assert_equal "2025-01-15T10:30:00Z", line.to_s
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def test_to_s_uses_tab_for_regex_delimiter
|
|
471
|
+
line = Zone::FieldLine.parse("foo bar baz") # Whitespace
|
|
472
|
+
|
|
473
|
+
assert_match(/\t/, line.to_s) # Should use tab in output
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def test_to_a_returns_fields_array
|
|
477
|
+
line = Zone::FieldLine.parse("a,b,c")
|
|
478
|
+
|
|
479
|
+
assert_equal ["a", "b", "c"], line.to_a
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def test_to_h_with_mapping
|
|
483
|
+
mapping = Zone::FieldMapping.from_fields(["x", "y", "z"])
|
|
484
|
+
line = Zone::FieldLine.parse(
|
|
485
|
+
"1,2,3",
|
|
486
|
+
mapping: mapping
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
expected = { "x" => "1", "y" => "2", "z" => "3" }
|
|
490
|
+
assert_equal expected, line.to_h
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def test_to_h_without_mapping_returns_empty
|
|
494
|
+
line = Zone::FieldLine.parse("a,b,c")
|
|
495
|
+
|
|
496
|
+
assert_empty line.to_h
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def test_infer_delimiter_comma
|
|
500
|
+
delimiter = Zone::FieldLine.infer_delimiter(
|
|
501
|
+
"a,b,c",
|
|
502
|
+
explicit: nil
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
assert_equal ",", delimiter
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def test_infer_delimiter_tab
|
|
509
|
+
delimiter = Zone::FieldLine.infer_delimiter(
|
|
510
|
+
"a\tb\tc",
|
|
511
|
+
explicit: nil
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
assert_equal "\t", delimiter
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def test_infer_delimiter_whitespace
|
|
518
|
+
delimiter = Zone::FieldLine.infer_delimiter(
|
|
519
|
+
"a b c",
|
|
520
|
+
explicit: nil
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
assert_instance_of Regexp, delimiter
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def test_infer_delimiter_explicit_overrides
|
|
527
|
+
delimiter = Zone::FieldLine.infer_delimiter(
|
|
528
|
+
"a,b,c",
|
|
529
|
+
explicit: "|"
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
assert_equal "|", delimiter
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def test_chainable_transformations
|
|
536
|
+
line = Zone::FieldLine.parse("a,b,c")
|
|
537
|
+
|
|
538
|
+
result = line
|
|
539
|
+
.transform(1, &:upcase)
|
|
540
|
+
.transform(2, &:upcase)
|
|
541
|
+
.transform(3, &:upcase)
|
|
542
|
+
|
|
543
|
+
assert_same line, result
|
|
544
|
+
assert_equal "A,B,C", line.to_s
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## 5. test/zone/test_zone_module.rb
|
|
552
|
+
|
|
553
|
+
### **Purpose**: Test Zone.find module method
|
|
554
|
+
|
|
555
|
+
### **Assertions to Use**:
|
|
556
|
+
- `assert_instance_of` - Verify TZInfo::Timezone returned
|
|
557
|
+
- `assert_raises` - Test invalid timezone
|
|
558
|
+
- `assert_equal` - Verify correct timezone found
|
|
559
|
+
|
|
560
|
+
### **Test Cases**:
|
|
561
|
+
|
|
562
|
+
```ruby
|
|
563
|
+
# frozen_string_literal: true
|
|
564
|
+
|
|
565
|
+
require "test_helper"
|
|
566
|
+
|
|
567
|
+
class TestZoneModule < Minitest::Test
|
|
568
|
+
def test_find_exact_timezone_name
|
|
569
|
+
tz = Zone.find("America/New_York")
|
|
570
|
+
|
|
571
|
+
assert_instance_of TZInfo::Timezone, tz
|
|
572
|
+
assert_equal "America/New_York", tz.identifier
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def test_find_utc
|
|
576
|
+
tz = Zone.find("UTC")
|
|
577
|
+
|
|
578
|
+
assert_instance_of TZInfo::Timezone, tz
|
|
579
|
+
assert_equal "UTC", tz.identifier
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def test_find_fuzzy_tokyo
|
|
583
|
+
tz = Zone.find("tokyo")
|
|
584
|
+
|
|
585
|
+
assert_instance_of TZInfo::Timezone, tz
|
|
586
|
+
assert_equal "Asia/Tokyo", tz.identifier
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def test_find_fuzzy_new_york
|
|
590
|
+
tz = Zone.find("new york")
|
|
591
|
+
|
|
592
|
+
assert_instance_of TZInfo::Timezone, tz
|
|
593
|
+
assert_match(/New_York/, tz.identifier)
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def test_find_us_timezone
|
|
597
|
+
tz = Zone.find("eastern")
|
|
598
|
+
|
|
599
|
+
assert_instance_of TZInfo::Timezone, tz
|
|
600
|
+
assert_match(/^US\//, tz.identifier)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def test_find_returns_nil_for_invalid
|
|
604
|
+
tz = Zone.find("not_a_real_timezone_12345")
|
|
605
|
+
|
|
606
|
+
assert_nil tz
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def test_find_case_insensitive
|
|
610
|
+
tz1 = Zone.find("Tokyo")
|
|
611
|
+
tz2 = Zone.find("TOKYO")
|
|
612
|
+
tz3 = Zone.find("tokyo")
|
|
613
|
+
|
|
614
|
+
assert_equal tz1.identifier, tz2.identifier
|
|
615
|
+
assert_equal tz2.identifier, tz3.identifier
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
## 6. test/integration/test_cli_integration.rb
|
|
623
|
+
|
|
624
|
+
### **Purpose**: End-to-end testing of actual CLI commands
|
|
625
|
+
|
|
626
|
+
### **Assertions to Use**:
|
|
627
|
+
- `capture_subprocess_io` - Run actual commands and capture output
|
|
628
|
+
- `assert_match` - Verify output format
|
|
629
|
+
- `assert_equal` - Verify exact output
|
|
630
|
+
- `refute_match` - Ensure errors don't occur
|
|
631
|
+
|
|
632
|
+
### **Test Cases**:
|
|
633
|
+
|
|
634
|
+
```ruby
|
|
635
|
+
# frozen_string_literal: true
|
|
636
|
+
|
|
637
|
+
require "test_helper"
|
|
638
|
+
|
|
639
|
+
class TestCLIIntegration < Minitest::Test
|
|
640
|
+
ZONE_COMMAND = File.expand_path("../../exe/zone", __dir__)
|
|
641
|
+
|
|
642
|
+
def run_zone(input, *args)
|
|
643
|
+
capture_subprocess_io do
|
|
644
|
+
IO.popen(
|
|
645
|
+
[ZONE_COMMAND, *args],
|
|
646
|
+
"r+",
|
|
647
|
+
err: [:child, :out]
|
|
648
|
+
) do |io|
|
|
649
|
+
io.write(input)
|
|
650
|
+
io.close_write
|
|
651
|
+
puts io.read
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def test_basic_timestamp_conversion
|
|
657
|
+
out, err = run_zone(
|
|
658
|
+
"2025-01-15T10:30:00Z\n",
|
|
659
|
+
"--utc"
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
assert_match(/2025-01-15T10:30:00Z/, out)
|
|
663
|
+
assert_empty err
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def test_unix_timestamp_to_pretty
|
|
667
|
+
out, err = run_zone(
|
|
668
|
+
"1736937000\n",
|
|
669
|
+
"--utc",
|
|
670
|
+
"--pretty"
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
assert_match(/Jan 15, 2025/, out)
|
|
674
|
+
assert_match(/10:30 AM/, out)
|
|
675
|
+
assert_empty err
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def test_timezone_conversion_tokyo
|
|
679
|
+
out, err = run_zone(
|
|
680
|
+
"2025-01-15T10:30:00Z\n",
|
|
681
|
+
"--zone",
|
|
682
|
+
"tokyo"
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
assert_match(/19:30:00\+09:00/, out)
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
def test_field_parsing_multiple_fields
|
|
689
|
+
out, err = run_zone(
|
|
690
|
+
"test 1736937000 data\n",
|
|
691
|
+
"--field",
|
|
692
|
+
"2",
|
|
693
|
+
"--utc"
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
assert_match(/test/, out)
|
|
697
|
+
assert_match(/2025-01-15T10:30:00Z/, out)
|
|
698
|
+
assert_match(/data/, out)
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def test_csv_with_headers
|
|
702
|
+
input = <<~CSV
|
|
703
|
+
name,timestamp,value
|
|
704
|
+
foo,2025-01-15T10:30:00Z,100
|
|
705
|
+
bar,1736937000,200
|
|
706
|
+
CSV
|
|
707
|
+
|
|
708
|
+
out, err = run_zone(
|
|
709
|
+
input,
|
|
710
|
+
"--headers",
|
|
711
|
+
"--field",
|
|
712
|
+
"2",
|
|
713
|
+
"--zone",
|
|
714
|
+
"tokyo"
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
assert_match(/name,timestamp,value/, out)
|
|
718
|
+
assert_match(/foo/, out)
|
|
719
|
+
assert_match(/\+09:00/, out)
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
def test_help_output
|
|
723
|
+
out, err = run_zone("", "--help")
|
|
724
|
+
|
|
725
|
+
assert_match(/Usage: zone/, out)
|
|
726
|
+
assert_match(/--iso8601/, out)
|
|
727
|
+
assert_match(/--zone/, out)
|
|
728
|
+
assert_match(/--field/, out)
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def test_multiple_timestamps_as_arguments
|
|
732
|
+
out, err = capture_subprocess_io do
|
|
733
|
+
system(
|
|
734
|
+
ZONE_COMMAND,
|
|
735
|
+
"--utc",
|
|
736
|
+
"2025-01-15T10:30:00Z",
|
|
737
|
+
"1736937000",
|
|
738
|
+
out: :out,
|
|
739
|
+
err: :err
|
|
740
|
+
)
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
lines = out.split("\n")
|
|
744
|
+
assert_equal 2, lines.count
|
|
745
|
+
assert_match(/2025-01-15/, lines[0])
|
|
746
|
+
assert_match(/2025-01-15/, lines[1])
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
def test_verbose_logging
|
|
750
|
+
out, err = run_zone(
|
|
751
|
+
"2025-01-15T10:30:00Z\n",
|
|
752
|
+
"--zone",
|
|
753
|
+
"tokyo",
|
|
754
|
+
"--verbose"
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
assert_match(/Using time zone/, err)
|
|
758
|
+
assert_match(/Tokyo/, err)
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
def test_strftime_format
|
|
762
|
+
out, err = run_zone(
|
|
763
|
+
"2025-01-15T10:30:00Z\n",
|
|
764
|
+
"--utc",
|
|
765
|
+
"--strftime",
|
|
766
|
+
"%Y-%m-%d"
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
assert_equal "2025-01-15\n", out
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
def test_unix_output_format
|
|
773
|
+
out, err = run_zone(
|
|
774
|
+
"2025-01-15T10:30:00Z\n",
|
|
775
|
+
"--utc",
|
|
776
|
+
"--unix"
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
assert_equal "1736937000\n", out
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def test_local_timezone
|
|
783
|
+
out, err = run_zone(
|
|
784
|
+
"2025-01-15T10:30:00Z\n",
|
|
785
|
+
"--local"
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
# Should convert to local time (output will vary by system)
|
|
789
|
+
assert_match(/2025-01-15/, out)
|
|
790
|
+
refute_match(/\+00:00/, out) # Should not be UTC
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def test_delimiter_inference_tab
|
|
794
|
+
out, err = run_zone(
|
|
795
|
+
"foo\t1736937000\tbar\n",
|
|
796
|
+
"--field",
|
|
797
|
+
"2",
|
|
798
|
+
"--utc"
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
assert_match(/foo/, out)
|
|
802
|
+
assert_match(/2025-01-15/, out)
|
|
803
|
+
assert_match(/bar/, out)
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
def test_explicit_delimiter
|
|
807
|
+
out, err = run_zone(
|
|
808
|
+
"foo|1736937000|bar\n",
|
|
809
|
+
"--field",
|
|
810
|
+
"2",
|
|
811
|
+
"--delimiter",
|
|
812
|
+
"|",
|
|
813
|
+
"--utc"
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
assert_match(/foo/, out)
|
|
817
|
+
assert_match(/2025-01-15/, out)
|
|
818
|
+
assert_match(/bar/, out)
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
def test_no_input_uses_current_time
|
|
822
|
+
out, err = capture_subprocess_io do
|
|
823
|
+
system(
|
|
824
|
+
ZONE_COMMAND,
|
|
825
|
+
"--utc",
|
|
826
|
+
in: :close,
|
|
827
|
+
out: :out,
|
|
828
|
+
err: :err
|
|
829
|
+
)
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
# Should output current time
|
|
833
|
+
assert_match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, out)
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
def test_invalid_timestamp_logs_warning
|
|
837
|
+
out, err = run_zone(
|
|
838
|
+
"not_a_valid_timestamp\n",
|
|
839
|
+
"--utc",
|
|
840
|
+
"--verbose"
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
assert_match(/Could not parse/, err)
|
|
844
|
+
assert_match(/Skipping/, err)
|
|
845
|
+
end
|
|
846
|
+
end
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
851
|
+
## Summary of Assertions Used
|
|
852
|
+
|
|
853
|
+
Based on Minitest documentation:
|
|
854
|
+
|
|
855
|
+
### **Equality & Type Checks**
|
|
856
|
+
- ✅ `assert_equal` - Most common, verify expected values
|
|
857
|
+
- ✅ `refute_equal` - Verify values are different
|
|
858
|
+
- ✅ `assert_same` - Verify object identity (for chainable methods)
|
|
859
|
+
- ✅ `assert_instance_of` - Verify exact class
|
|
860
|
+
- ✅ `assert_nil` - Verify nil values
|
|
861
|
+
- ✅ `refute_nil` - Verify non-nil values
|
|
862
|
+
|
|
863
|
+
### **Collections**
|
|
864
|
+
- ✅ `assert_includes` - Verify array/hash membership
|
|
865
|
+
- ✅ `assert_empty` - Verify empty collections
|
|
866
|
+
- ✅ `refute_empty` - Verify non-empty collections
|
|
867
|
+
|
|
868
|
+
### **Pattern Matching**
|
|
869
|
+
- ✅ `assert_match` - Verify regex patterns in strings
|
|
870
|
+
- ✅ `refute_match` - Verify patterns don't match
|
|
871
|
+
|
|
872
|
+
### **Exceptions**
|
|
873
|
+
- ✅ `assert_raises` - Verify error handling
|
|
874
|
+
|
|
875
|
+
### **Numeric**
|
|
876
|
+
- ✅ `assert_in_delta` - Compare floats with tolerance
|
|
877
|
+
|
|
878
|
+
### **Methods**
|
|
879
|
+
- ✅ `assert_respond_to` - Verify API surface
|
|
880
|
+
|
|
881
|
+
### **IO**
|
|
882
|
+
- ✅ `capture_subprocess_io` - Test actual CLI commands
|
|
883
|
+
|
|
884
|
+
---
|
|
885
|
+
|
|
886
|
+
## Test Execution
|
|
887
|
+
|
|
888
|
+
```bash
|
|
889
|
+
# Run all tests
|
|
890
|
+
rake test
|
|
891
|
+
|
|
892
|
+
# Run specific test file
|
|
893
|
+
ruby test/zone/test_timestamp.rb
|
|
894
|
+
|
|
895
|
+
# Run specific test
|
|
896
|
+
ruby test/zone/test_timestamp.rb --name test_parse_unix_timestamp_seconds
|
|
897
|
+
|
|
898
|
+
# Run with verbose output
|
|
899
|
+
ruby test/zone/test_timestamp.rb --verbose
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
---
|
|
903
|
+
|
|
904
|
+
## Coverage Goals
|
|
905
|
+
|
|
906
|
+
- **Unit tests**: 100% coverage of public methods
|
|
907
|
+
- **Integration tests**: All CLI flags and combinations
|
|
908
|
+
- **Edge cases**: Invalid input, missing fields, errors
|
|
909
|
+
- **Documentation**: Every assertion type used appropriately
|
|
910
|
+
|
|
911
|
+
This plan follows **Rule 1** by reading all Minitest documentation and using the appropriate assertion types for each test case.
|