Support RFC3339 timestamps for wslc container logs --since/--until (#40826)

* Add --timestamps, --since, and --until flags to wslc container logs Wire up the existing WSLCLogsFlagsTimestamps flag and Since/Until parameters from the IWSLCContainer::Logs COM interface to the CLI. Previously these were hardcoded to 0/disabled despite the backend already supporting them. Changes: - Add Timestamps (flag, -t), Since (value), Until (value) argument types - Add ULONGLONG validation for --since and --until - Register new args on the container logs command - Pass timestamps flag and since/until values through to the COM call - Add localization strings with {Locked} comments for the new flags - Add 8 command-line parsing unit test cases - Add 6 end-to-end test methods covering timestamps, since, until, combined usage, and short flag (-t) Usage: wslc container logs --timestamps mycontainer wslc container logs --since 1700000000 mycontainer wslc container logs --until 1700001000 mycontainer wslc container logs -t -f --since 0 --tail 50 mycontainer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add localization placeholders for new log flag strings Add WSLCCLI_TimestampsArgDescription, WSLCCLI_SinceArgDescription, and WSLCCLI_UntilArgDescription to all 21 non-en-US locale files with English placeholder values. These will be translated by the localization team. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Support RFC3339 timestamps for wslc container logs --since/--until Add RFC3339 date-time parsing (e.g. 2024-01-15T10:30:00Z) as an alternative to Unix epoch seconds for the --since and --until arguments of 'wsl container logs'. This aligns with the Docker CLI timestamp format. The parser handles: - UTC timestamps with 'Z' suffix - Timezone offsets in +HH:MM and +HHMM formats - Fractional seconds (ignored for epoch conversion) - Validation of date/time component ranges Invalid inputs (pre-1970 dates, malformed strings, trailing garbage) are rejected with a descriptive error message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback: use std::chrono::parse, revert non-en-US locales - Replace custom RFC3339 parser with std::chrono::parse, matching the pattern used in WSLCContainer.cpp's ParseDockerTimestamp - Pre-validate year_month_day to reject impossible dates (e.g. Feb 31) that std::chrono::parse normalizes instead of rejecting - Strip fractional seconds before parsing (second-level precision only) - Normalize Z suffix to +00:00 to avoid %Z greedy timezone matching - Reject trailing garbage after timezone designator via stream.peek() - Revert non-en-US locale files (generated by localization pipeline) - Strengthen E2E timestamp tests to validate RFC3339 structure instead of just checking for 'T' character - Add test cases for invalid day-of-month and dot-without-digits Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address code review: simplify TryParseRfc3339 and improve tests - Parse into sys_time<milliseconds> to handle fractional seconds natively instead of manually stripping them (per @OneBlue review) - Remove manual year_month_day pre-validation since std::chrono::parse already rejects invalid dates on MSVC (confirmed via godbolt) - Add +HHMM (no colon) test case showing it is correctly rejected - Fix AllOptionsCombined test to validate RFC3339 structure properly (check separator positions instead of just finding 'T') - Document that since=0/until=0 means 'unset' in Docker API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add unit tests for TryParseRfc3339 timestamp parsing Per @dkbennett review: add dedicated unit tests exercising the timestamp parsing logic directly via GetTimestampFromString. Tests cover: - Valid Unix epoch integers - Valid RFC3339 with Z, lowercase z, and +/-HH:MM offsets - Fractional seconds (truncated to whole seconds) - Invalid inputs: bad month/hour/day, missing TZ, date-only, trailing chars, +HHMM (no colon), dot with no digits, random text, pre-1970 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix RFC3339 timestamp parsing tests and validation - Fix incorrect expected Unix timestamps in unit tests (off by 3600s due to computation errors in the original values) - Add date validation to reject invalid calendar dates (e.g. Feb 31) that std::chrono::parse silently wraps - Reject bare dot with no fractional digits (e.g. '10:30:00.Z') - Use nanosecond precision for std::chrono::parse to properly consume long fractional seconds (e.g. '.123456789') Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Pooja Trivedi <trivedipooja@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Pooja Trivedi committed Jun 19, 2026 at 16:09 UTC 9d64cb0743ff2b776c33b8939797b4ef3a5ae12d
7 files changed +230 -10
localization/strings/en-US/Resources.resw
+6 -2
@@ -2925,10 +2925,10 @@ On first run, creates the file with all settings commented out at their defaults
2925 <value>Show timestamps in log output</value>
2926 </data>
2927 <data name="WSLCCLI_SinceArgDescription" xml:space="preserve">
2928 - <value>Show logs since timestamp (e.g. unix epoch seconds)</value>
2928 + <value>Show logs since timestamp (Unix epoch seconds or RFC3339, e.g. 2024-01-15T10:30:00Z)</value>
2929 </data>
2930 <data name="WSLCCLI_UntilArgDescription" xml:space="preserve">
2931 - <value>Show logs before timestamp (e.g. unix epoch seconds)</value>
2931 + <value>Show logs before timestamp (Unix epoch seconds or RFC3339, e.g. 2024-01-15T10:30:00Z)</value>
2932 </data>
2933 <data name="WSLCCLI_TargetArgDescription" xml:space="preserve">
2934 <value>New image reference in the image-name[:tag] format</value>
@@ -3040,6 +3040,10 @@ On first run, creates the file with all settings commented out at their defaults
3040 <value>Invalid {} argument value: {}</value>
3041 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3042 </data>
3043 + <data name="WSLCCLI_InvalidTimestampArgumentError" xml:space="preserve">
3044 + <value>Invalid {} argument value: '{}'. Expected a Unix epoch timestamp or an RFC3339 date-time (e.g. 2024-01-15T10:30:00Z)</value>
3045 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated{Locked="2024-01-15T10:30:00Z"}{Locked="RFC3339"}</comment>
3046 + </data>
3047 <data name="WSLCCLI_SignalOutOfRangeError" xml:space="preserve">
3048 <value>Invalid {} value: {} is out of valid range ({}-{}).</value>
3049 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/windows/wslc/arguments/ArgumentValidation.cpp
+102 -2
@@ -20,7 +20,9 @@ Abstract:
20 #include "Exceptions.h"
21 #include "Localization.h"
22 #include <charconv>
23 +#include <chrono>
24 #include <format>
25 +#include <sstream>
26 #include <unordered_map>
27 #include <wslc.h>
28
@@ -72,11 +74,11 @@ void Argument::Validate(const ArgMap& execArgs) const
74 break;
75
76 case ArgType::Since:
75 - validation::ValidateIntegerFromString<ULONGLONG>(execArgs.GetAll<ArgType::Since>(), m_name);
77 + validation::ValidateTimestamp(execArgs.GetAll<ArgType::Since>(), m_name);
78 break;
79
80 case ArgType::Until:
79 - validation::ValidateIntegerFromString<ULONGLONG>(execArgs.GetAll<ArgType::Until>(), m_name);
81 + validation::ValidateTimestamp(execArgs.GetAll<ArgType::Until>(), m_name);
82 break;
83
84 case ArgType::Last:
@@ -233,6 +235,104 @@ WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring
235 return static_cast<WSLCSignal>(signalValue);
236 }
237
238 +// Parses an RFC3339 timestamp (e.g. "2024-01-15T10:30:00Z" or "2024-01-15T10:30:00+05:30")
239 +// into a ULONGLONG Unix epoch seconds value using std::chrono::parse.
240 +// Note: +HHMM (no colon) offsets are not supported; use +HH:MM format.
241 +static std::optional<ULONGLONG> TryParseRfc3339(const std::string& input)
242 +{
243 + std::string normalized = input;
244 +
245 + // Normalize trailing 'Z'/'z' to '+00:00' so %Ez can parse it uniformly.
246 + if (!normalized.empty() && (normalized.back() == 'Z' || normalized.back() == 'z'))
247 + {
248 + normalized.pop_back();
249 + normalized += "+00:00";
250 + }
251 +
252 + // Reject bare dot with no fractional digits (e.g. "10:30:00.+00:00") since
253 + // std::chrono::parse is lenient about this.
254 + auto dotPos = normalized.find('.');
255 + if (dotPos != std::string::npos && (dotPos + 1 >= normalized.size() || !std::isdigit(normalized[dotPos + 1])))
256 + {
257 + return std::nullopt;
258 + }
259 +
260 + // Pre-validate day-of-month since std::chrono::parse silently wraps invalid dates (e.g. Feb 31 → Mar 2).
261 + if (normalized.size() >= 10 && normalized[4] == '-' && normalized[7] == '-')
262 + {
263 + int year = 0, month = 0, day = 0;
264 + auto yResult = std::from_chars(normalized.data(), normalized.data() + 4, year);
265 + auto mResult = std::from_chars(normalized.data() + 5, normalized.data() + 7, month);
266 + auto dResult = std::from_chars(normalized.data() + 8, normalized.data() + 10, day);
267 +
268 + if (yResult.ec == std::errc() && mResult.ec == std::errc() && dResult.ec == std::errc())
269 + {
270 + auto ymd = std::chrono::year{year} / std::chrono::month{static_cast<unsigned>(month)} /
271 + std::chrono::day{static_cast<unsigned>(day)};
272 + if (!ymd.ok())
273 + {
274 + return std::nullopt;
275 + }
276 + }
277 + }
278 +
279 + // Parse into nanosecond precision so fractional seconds (e.g. ".123456789") are consumed
280 + // by std::chrono::parse rather than requiring manual stripping.
281 + std::chrono::sys_time<std::chrono::nanoseconds> utcTime;
282 + std::istringstream stream(normalized);
283 + stream >> std::chrono::parse("%FT%T%Ez", utcTime);
284 + if (stream.fail())
285 + {
286 + return std::nullopt;
287 + }
288 +
289 + // Reject if there are trailing characters after the parsed timestamp
290 + if (stream.peek() != std::istringstream::traits_type::eof())
291 + {
292 + return std::nullopt;
293 + }
294 +
295 + auto epochSeconds = std::chrono::duration_cast<std::chrono::seconds>(utcTime.time_since_epoch()).count();
296 + if (epochSeconds < 0)
297 + {
298 + return std::nullopt;
299 + }
300 +
301 + return static_cast<ULONGLONG>(epochSeconds);
302 +}
303 +
304 +void ValidateTimestamp(const std::vector<std::wstring>& values, const std::wstring& argName)
305 +{
306 + for (const auto& value : values)
307 + {
308 + std::ignore = GetTimestampFromString(value, argName);
309 + }
310 +}
311 +
312 +ULONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring& argName)
313 +{
314 + std::string narrowValue = wsl::windows::common::string::WideToMultiByte(value);
315 +
316 + // Try integer (Unix epoch seconds) first
317 + ULONGLONG intValue{};
318 + const char* begin = narrowValue.c_str();
319 + const char* end = begin + narrowValue.size();
320 + auto result = std::from_chars(begin, end, intValue);
321 + if (result.ec == std::errc() && result.ptr == end)
322 + {
323 + return intValue;
324 + }
325 +
326 + // Try RFC3339 timestamp
327 + auto rfc3339Value = TryParseRfc3339(narrowValue);
328 + if (rfc3339Value.has_value())
329 + {
330 + return rfc3339Value.value();
331 + }
332 +
333 + throw ArgumentException(Localization::WSLCCLI_InvalidTimestampArgumentError(argName, value));
334 +}
335 +
336 void ValidateFormatTypeFromString(const std::vector<std::wstring>& values, const std::wstring& argName)
337 {
338 for (const auto& value : values)
src/windows/wslc/arguments/ArgumentValidation.h
+2
@@ -66,6 +66,8 @@ WSLCSignal GetWSLCSignalFromString(const std::wstring& input, const std::wstring
66 void ValidateMemorySize(const std::vector<std::wstring>& values, const std::wstring& argName);
67 int64_t GetMemorySizeFromString(const std::wstring& input, const std::wstring& argName = {});
68
69 +void ValidateTimestamp(const std::vector<std::wstring>& values, const std::wstring& argName);
70 +ULONGLONG GetTimestampFromString(const std::wstring& value, const std::wstring& argName = {});
71 void ValidateNanoCpus(const std::vector<std::wstring>& values, const std::wstring& argName);
72 int64_t GetNanoCpusFromString(const std::wstring& input, const std::wstring& argName = {});
73
src/windows/wslc/tasks/ContainerTasks.cpp
+5 -2
@@ -776,16 +776,19 @@ void ViewContainerLogs(CLIExecutionContext& context)
776 tail = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Tail>());
777 }
778
779 + // N.B. since=0 and until=0 mean "unset" — the Docker API omits the parameter when the value is 0,
780 + // which is equivalent to "no lower/upper bound". This matches Docker CLI behavior where
781 + // `docker logs --since 0` returns all logs and `docker logs --until 0` applies no upper bound.
782 ULONGLONG since = 0;
783 if (context.Args.Contains(ArgType::Since))
784 {
782 - since = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Since>());
785 + since = validation::GetTimestampFromString(context.Args.Get<ArgType::Since>());
786 }
787
788 ULONGLONG until = 0;
789 if (context.Args.Contains(ArgType::Until))
790 {
788 - until = validation::GetIntegerFromString<ULONGLONG>(context.Args.Get<ArgType::Until>());
791 + until = validation::GetTimestampFromString(context.Args.Get<ArgType::Until>());
792 }
793
794 ContainerService::Logs(session, WideToMultiByte(containerId), follow, timestamps, since, until, tail);
test/windows/wslc/CommandLineTestCases.h
+12
@@ -207,6 +207,18 @@ COMMAND_LINE_TEST_CASE(L"container logs --until 1700000000 cont1", L"logs", true
207 COMMAND_LINE_TEST_CASE(L"container logs --since 1700000000 --until 1700001000 cont1", L"logs", true)
208 COMMAND_LINE_TEST_CASE(L"container logs --since abc cont1", L"logs", false)
209 COMMAND_LINE_TEST_CASE(L"container logs --until abc cont1", L"logs", false)
210 +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00Z cont1", L"logs", true)
211 +COMMAND_LINE_TEST_CASE(L"container logs --until 2024-01-15T10:30:00Z cont1", L"logs", true)
212 +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00+05:30 cont1", L"logs", true)
213 +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00.123456789Z cont1", L"logs", true)
214 +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-13-15T10:30:00Z cont1", L"logs", false)
215 +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T25:30:00Z cont1", L"logs", false)
216 +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15 cont1", L"logs", false)
217 +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00Zextra cont1", L"logs", false)
218 +COMMAND_LINE_TEST_CASE(L"container logs --since 1960-01-15T10:30:00Z cont1", L"logs", false)
219 +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-02-31T10:30:00Z cont1", L"logs", false)
220 +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00.Z cont1", L"logs", false)
221 +COMMAND_LINE_TEST_CASE(L"container logs --since 2024-01-15T10:30:00+0530 cont1", L"logs", false)
222 COMMAND_LINE_TEST_CASE(L"container logs --follow --timestamps --since 100 --tail 5 cont1", L"logs", true)
223
224 // Image command
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp
+61
@@ -222,5 +222,66 @@ class WSLCCLIArgumentUnitTests
222 argsContainer.Remove(ArgType::ForwardArgs);
223 VERIFY_ARE_EQUAL(argsContainer.GetCount(), 0);
224 }
225 + // Timestamp parsing unit tests (exercises TryParseRfc3339 and integer path via GetTimestampFromString)
226 +
227 + TEST_METHOD(ValidateTimestamp_ValidUnixEpochSeconds)
228 + {
229 + // Integer timestamps should parse directly
230 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"0"), 0ULL);
231 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1700000000"), 1700000000ULL);
232 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1"), 1ULL);
233 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"9999999999"), 9999999999ULL);
234 + }
235 +
236 + TEST_METHOD(ValidateTimestamp_ValidRfc3339_UTC)
237 + {
238 + // Basic UTC timestamps with Z suffix
239 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00Z"), 1705314600ULL);
240 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"1970-01-01T00:00:00Z"), 0ULL);
241 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00z"), 1705314600ULL); // lowercase z
242 + }
243 +
244 + TEST_METHOD(ValidateTimestamp_ValidRfc3339_WithOffset)
245 + {
246 + // Timestamps with timezone offsets (+HH:MM / -HH:MM)
247 + // 2024-01-15T10:30:00+05:30 = 2024-01-15T05:00:00Z = 1705294800
248 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00+05:30"), 1705294800ULL);
249 + // 2024-01-15T10:30:00-05:00 = 2024-01-15T15:30:00Z = 1705332600
250 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00-05:00"), 1705332600ULL);
251 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00+00:00"), 1705314600ULL);
252 + }
253 +
254 + TEST_METHOD(ValidateTimestamp_ValidRfc3339_FractionalSeconds)
255 + {
256 + // Fractional seconds should be consumed (truncated to seconds)
257 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.123Z"), 1705314600ULL);
258 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.123456789Z"), 1705314600ULL);
259 + VERIFY_ARE_EQUAL(validation::GetTimestampFromString(L"2024-01-15T10:30:00.1+05:30"), 1705294800ULL);
260 + }
261 +
262 + TEST_METHOD(ValidateTimestamp_InvalidRfc3339_Rejected)
263 + {
264 + // Invalid month
265 + VERIFY_THROWS(validation::GetTimestampFromString(L"2024-13-15T10:30:00Z"), ArgumentException);
266 + // Invalid hour
267 + VERIFY_THROWS(validation::GetTimestampFromString(L"2024-01-15T25:30:00Z"), ArgumentException);
268 + // Invalid day (Feb 31)
269 + VERIFY_THROWS(validation::GetTimestampFromString(L"2024-02-31T10:30:00Z"), ArgumentException);
270 + // Missing timezone
271 + VERIFY_THROWS(validation::GetTimestampFromString(L"2024-01-15T10:30:00"), ArgumentException);
272 + // Date only (no time)
273 + VERIFY_THROWS(validation::GetTimestampFromString(L"2024-01-15"), ArgumentException);
274 + // Trailing characters
275 + VERIFY_THROWS(validation::GetTimestampFromString(L"2024-01-15T10:30:00Zextra"), ArgumentException);
276 + // +HHMM without colon (not supported by %Ez)
277 + VERIFY_THROWS(validation::GetTimestampFromString(L"2024-01-15T10:30:00+0530"), ArgumentException);
278 + // Dot with no fractional digits
279 + VERIFY_THROWS(validation::GetTimestampFromString(L"2024-01-15T10:30:00.Z"), ArgumentException);
280 + // Random text
281 + VERIFY_THROWS(validation::GetTimestampFromString(L"abc"), ArgumentException);
282 + VERIFY_THROWS(validation::GetTimestampFromString(L"not-a-timestamp"), ArgumentException);
283 + // Negative epoch (pre-1970)
284 + VERIFY_THROWS(validation::GetTimestampFromString(L"1960-01-15T10:30:00Z"), ArgumentException);
285 + }
286 };
287 } // namespace WSLCCLIArgumentUnitTests
test/windows/wslc/e2e/WSLCE2EContainerLogsTests.cpp
+42 -4
@@ -73,8 +73,13 @@ class WSLCE2EContainerLogsTests
73 VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"hello"));
74 auto lines = result.GetStdoutLines();
75 VERIFY_IS_TRUE(!lines.empty());
76 - // A timestamp line should have at least a 'T' character (ISO 8601 separator)
77 - VERIFY_IS_TRUE(lines[0].find(L'T') != std::wstring::npos);
76 + // Validate RFC3339 structure: YYYY-MM-DDTHH:MM:SS at the start of the line
77 + VERIFY_IS_TRUE(lines[0].size() >= 20);
78 + VERIFY_ARE_EQUAL(lines[0][4], L'-');
79 + VERIFY_ARE_EQUAL(lines[0][7], L'-');
80 + VERIFY_ARE_EQUAL(lines[0][10], L'T');
81 + VERIFY_ARE_EQUAL(lines[0][13], L':');
82 + VERIFY_ARE_EQUAL(lines[0][16], L':');
83 }
84
85 WSLC_TEST_METHOD(WSLCE2E_Container_Logs_TimestampsShortFlag)
@@ -91,7 +96,12 @@ class WSLCE2EContainerLogsTests
96 VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"world"));
97 auto lines = result.GetStdoutLines();
98 VERIFY_IS_TRUE(!lines.empty());
94 - VERIFY_IS_TRUE(lines[0].find(L'T') != std::wstring::npos);
99 + VERIFY_IS_TRUE(lines[0].size() >= 20);
100 + VERIFY_ARE_EQUAL(lines[0][4], L'-');
101 + VERIFY_ARE_EQUAL(lines[0][7], L'-');
102 + VERIFY_ARE_EQUAL(lines[0][10], L'T');
103 + VERIFY_ARE_EQUAL(lines[0][13], L':');
104 + VERIFY_ARE_EQUAL(lines[0][16], L':');
105 }
106
107 WSLC_TEST_METHOD(WSLCE2E_Container_Logs_Since)
@@ -111,6 +121,28 @@ class WSLCE2EContainerLogsTests
121 result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
122 }
123
124 + WSLC_TEST_METHOD(WSLCE2E_Container_Logs_SinceRfc3339)
125 + {
126 + // Run a container that outputs a line
127 + auto result =
128 + RunWslc(std::format(L"container run --name {} {} sh -c \"echo rfc3339test\"", WslcContainerName, DebianImage.NameAndTag()));
129 + result.Verify({.Stderr = L"", .ExitCode = 0});
130 +
131 + // Using --since with an RFC3339 timestamp in the past should return all logs
132 + result = RunWslc(std::format(L"container logs --since 2000-01-01T00:00:00Z {}", WslcContainerName));
133 + result.Verify({.Stderr = L"", .ExitCode = 0});
134 + VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"rfc3339test"));
135 +
136 + // Using --since with an RFC3339 timestamp far in the future should return no logs
137 + result = RunWslc(std::format(L"container logs --since 2099-12-31T23:59:59Z {}", WslcContainerName));
138 + result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
139 +
140 + // Using --since with an RFC3339 timestamp with timezone offset
141 + result = RunWslc(std::format(L"container logs --since 2000-01-01T00:00:00+00:00 {}", WslcContainerName));
142 + result.Verify({.Stderr = L"", .ExitCode = 0});
143 + VERIFY_IS_TRUE(result.StdoutContainsSubstring(L"rfc3339test"));
144 + }
145 +
146 WSLC_TEST_METHOD(WSLCE2E_Container_Logs_Until)
147 {
148 // Run a container that outputs a line
@@ -163,7 +195,13 @@ class WSLCE2EContainerLogsTests
195 {
196 if (!line.empty())
197 {
166 - VERIFY_IS_TRUE(line.find(L'T') != std::wstring::npos);
198 + // Validate RFC3339 structure: YYYY-MM-DDTHH:MM:SS at the start of the line
199 + VERIFY_IS_TRUE(line.size() >= 20);
200 + VERIFY_ARE_EQUAL(line[4], L'-');
201 + VERIFY_ARE_EQUAL(line[7], L'-');
202 + VERIFY_ARE_EQUAL(line[10], L'T');
203 + VERIFY_ARE_EQUAL(line[13], L':');
204 + VERIFY_ARE_EQUAL(line[16], L':');
205 }
206 }
207 }