Skip to content

Commit 6c9682b

Browse files
dfa1claude
andcommitted
test(writer): add TimeExtensionEncoder coverage
Was 63% line coverage with no writer-side test. Covers extensionId, dtype factories (default ms/I32, seconds/I32, nanos/I64), encodeAll for all four valid units (int[] for s/ms, long[] for us/ns), nullable null handling (NullableData + zero placeholder), the non-nullable null throw, and the Days-unit rejection guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 528372a commit 6c9682b

1 file changed

Lines changed: 167 additions & 0 deletions

File tree

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package io.github.dfa1.vortex.writer.encode;
2+
3+
import io.github.dfa1.vortex.core.DType;
4+
import io.github.dfa1.vortex.core.PType;
5+
import io.github.dfa1.vortex.core.VortexException;
6+
import io.github.dfa1.vortex.encoding.TimeUnit;
7+
import io.github.dfa1.vortex.extension.ExtensionId;
8+
import io.github.dfa1.vortex.extension.TimeDtype;
9+
import org.junit.jupiter.api.Test;
10+
11+
import java.nio.ByteBuffer;
12+
import java.time.LocalTime;
13+
import java.util.Arrays;
14+
import java.util.List;
15+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
18+
19+
class TimeExtensionEncoderTest {
20+
21+
private static final TimeExtensionEncoder SUT = TimeExtensionEncoder.INSTANCE;
22+
private static final LocalTime T = LocalTime.of(1, 2, 3, 456_000_000); // 01:02:03.456
23+
24+
@Test
25+
void extensionId_isVortexTime() {
26+
// Given / When / Then
27+
assertThat(SUT.extensionId()).isEqualTo(ExtensionId.VORTEX_TIME);
28+
}
29+
30+
@Test
31+
void dtype_default_isMillisecondsOverI32() {
32+
// Given / When
33+
DType.Extension result = SUT.dtype(false);
34+
35+
// Then
36+
assertThat(TimeDtype.readUnit(result)).isEqualTo(TimeUnit.Milliseconds);
37+
assertThat(result.storageDType()).isEqualTo(new DType.Primitive(PType.I32, false));
38+
}
39+
40+
@Test
41+
void dtype_seconds_usesI32() {
42+
// Given / When
43+
DType.Extension result = SUT.dtype(TimeUnit.Seconds, true);
44+
45+
// Then
46+
assertThat(TimeDtype.readUnit(result)).isEqualTo(TimeUnit.Seconds);
47+
assertThat(result.storageDType()).isEqualTo(new DType.Primitive(PType.I32, true));
48+
}
49+
50+
@Test
51+
void dtype_nanoseconds_usesI64() {
52+
// Given / When
53+
DType.Extension result = SUT.dtype(TimeUnit.Nanoseconds, false);
54+
55+
// Then
56+
assertThat(TimeDtype.readUnit(result)).isEqualTo(TimeUnit.Nanoseconds);
57+
assertThat(result.storageDType()).isEqualTo(new DType.Primitive(PType.I64, false));
58+
}
59+
60+
@Test
61+
void encodeAll_seconds_returnsIntArray() {
62+
// Given seconds resolution truncates sub-second precision
63+
DType.Extension dtype = SUT.dtype(TimeUnit.Seconds, false);
64+
65+
// When
66+
Object result = SUT.encodeAll(dtype, List.of(T));
67+
68+
// Then
69+
assertThat(result).isInstanceOf(int[].class);
70+
assertThat((int[]) result).containsExactly(1 * 3600 + 2 * 60 + 3);
71+
}
72+
73+
@Test
74+
void encodeAll_milliseconds_returnsIntArray() {
75+
// Given
76+
DType.Extension dtype = SUT.dtype(TimeUnit.Milliseconds, false);
77+
78+
// When
79+
Object result = SUT.encodeAll(dtype, List.of(T));
80+
81+
// Then
82+
long expectedMs = (1 * 3600 + 2 * 60 + 3) * 1000L + 456;
83+
assertThat(result).isInstanceOf(int[].class);
84+
assertThat((int[]) result).containsExactly((int) expectedMs);
85+
}
86+
87+
@Test
88+
void encodeAll_microseconds_returnsLongArray() {
89+
// Given
90+
DType.Extension dtype = SUT.dtype(TimeUnit.Microseconds, false);
91+
92+
// When
93+
Object result = SUT.encodeAll(dtype, List.of(T));
94+
95+
// Then
96+
assertThat(result).isInstanceOf(long[].class);
97+
assertThat((long[]) result).containsExactly(T.toNanoOfDay() / 1_000L);
98+
}
99+
100+
@Test
101+
void encodeAll_nanoseconds_returnsLongArray() {
102+
// Given
103+
DType.Extension dtype = SUT.dtype(TimeUnit.Nanoseconds, false);
104+
105+
// When
106+
Object result = SUT.encodeAll(dtype, List.of(T));
107+
108+
// Then
109+
assertThat(result).isInstanceOf(long[].class);
110+
assertThat((long[]) result).containsExactly(T.toNanoOfDay());
111+
}
112+
113+
@Test
114+
void encodeAll_nullWithNullableInt_returnsNullableDataWithZeroPlaceholder() {
115+
// Given a null in a nullable column
116+
DType.Extension dtype = SUT.dtype(TimeUnit.Milliseconds, true);
117+
118+
// When
119+
Object result = SUT.encodeAll(dtype, Arrays.asList(T, null));
120+
121+
// Then storage carries a zero placeholder at the null position; validity marks it
122+
assertThat(result).isInstanceOf(NullableData.class);
123+
NullableData nd = (NullableData) result;
124+
int[] values = (int[]) nd.values();
125+
assertThat(values[1]).isZero();
126+
assertThat(nd.validity()).containsExactly(true, false);
127+
}
128+
129+
@Test
130+
void encodeAll_nullWithNullableLong_returnsNullableData() {
131+
// Given a null in a nullable μs column (long storage)
132+
DType.Extension dtype = SUT.dtype(TimeUnit.Microseconds, true);
133+
134+
// When
135+
Object result = SUT.encodeAll(dtype, Arrays.asList(null, T));
136+
137+
// Then
138+
NullableData nd = (NullableData) result;
139+
assertThat(nd.values()).isInstanceOf(long[].class);
140+
assertThat(nd.validity()).containsExactly(false, true);
141+
}
142+
143+
@Test
144+
void encodeAll_nullInNonNullableColumn_throws() {
145+
// Given a null in a non-nullable column
146+
DType.Extension dtype = SUT.dtype(TimeUnit.Milliseconds, false);
147+
148+
// When / Then
149+
assertThatThrownBy(() -> SUT.encodeAll(dtype, Arrays.asList(T, null)))
150+
.isInstanceOf(VortexException.class)
151+
.hasMessageContaining("non-nullable");
152+
}
153+
154+
@Test
155+
void encodeAll_daysUnit_throws() {
156+
// Given a hand-built Days-tagged dtype (TimeDtype.of rejects Days, so build directly)
157+
ByteBuffer meta = ByteBuffer.allocate(1);
158+
meta.put(0, (byte) TimeUnit.Days.ordinal());
159+
DType.Extension dtype = new DType.Extension(
160+
ExtensionId.VORTEX_TIME.id(), new DType.Primitive(PType.I32, false), meta, false);
161+
162+
// When / Then
163+
assertThatThrownBy(() -> SUT.encodeAll(dtype, List.of(T)))
164+
.isInstanceOf(VortexException.class)
165+
.hasMessageContaining("Days");
166+
}
167+
}

0 commit comments

Comments
 (0)