TASK 55 slice 1: the startup log is size-capped
The creator (2026-10-09): 'we need some sort of log-rotation so that we don't inadvertently fill up a user's drive.' startup.log was uncapped - 23MB and growing every few seconds, because FfmpegEncoder logs every non-progress ffmpeg stderr line. The policy lives in a new Helpers/LogRotation.cs as a pure function over a path (RollIfNeeded(logPath, maxBytes, retainedFiles)), so AppLog's 156 call sites keep their surface and the rule is testable on its own. At 5MB the live file rolls to startup.1.log and one rolled copy is kept - the FILE SET is bounded, not just the live file, so a long stream cannot grow the log without limit. A failed rotation degrades to the old behaviour instead of taking the app down. Pattern reference (cited): Serilog's file sink pairs fileSizeLimitBytes (the cap) with retainedFileCountLimit (how many rolled files to keep) - https://github.com/serilog/serilog-sinks-file Guard: ytLive.Tests/AppLogRotationTests.cs - the ONE test drives the real AppLog.Write against a temp dir (two internal seams: PathOverride, MaxBytes) and asserts the roll, the fresh live file, and that after a SECOND roll the directory still holds exactly startup.log + startup.1.log. Also fixed a stale catalog row: TASK 12 (master limiter) shipped 2026-08-15 and was still marked 'Queued'.
This commit is contained in:
+16
-6
@@ -70,6 +70,13 @@
|
||||
`ytLive.Tests/BorderColorPickerTests.cs` (model + panel + the **real widget**). The TASK 60
|
||||
guard (`BorderColorPaletteTests`) was updated to guard the new wiring (the drop-down is bound to
|
||||
the palette; no second list of colours anywhere).
|
||||
- **TASK 55 slice 1 — the log is capped (shipped 2026-10-22).** `startup.log` was uncapped (23 MB
|
||||
and growing every few seconds — ffmpeg logs every stderr line). Now it rolls to `startup.1.log`
|
||||
at 5 MB (one rolled copy kept), so the growth is bounded. The policy is
|
||||
`Helpers/LogRotation.cs` (a pure function over a path — `AppLog`'s 156-call-site surface is
|
||||
unchanged; the seams are two `internal` properties). Guard:
|
||||
`ytLive.Tests/AppLogRotationTests.cs`. Slices 2 (temp alert-clip cleanup) and 3 (low-disk
|
||||
guard before recording) are still open.
|
||||
- **The previous session (HA lights) produced NO ytLive code** — it was spent on the creator's Home
|
||||
Assistant light alert system (off-repo, `192.168.50.145`). See `MyMistakes.md` → 2026-10-21: the plan
|
||||
was one tweak to a verified-working script; the script was rewritten twice, the alert path broke, and
|
||||
@@ -258,13 +265,15 @@ Zombie entities deleted from the registry: `script.leak_strobe`, `script.doorbel
|
||||
`Helpers/OAuthCredentials.cs` were never tracked. ⚠️ the repo was **already public** (anonymous
|
||||
GET → 200, API `private = false`) when the scan ran — the gate came late, but nothing leaked.
|
||||
Method + verdict: `TASKS/task-56-secret-scan.md`.
|
||||
- TASK 55 disk hygiene (`startup.log` uncapped — 23MB and growing every 5s).
|
||||
- TASK 55 disk hygiene: slice 1 (log rotation) **shipped 2026-10-22** — see the At-a-glance bullet.
|
||||
Slices 2 (temp alert-clip cleanup) + 3 (low-disk guard before recording) are open. (The GOG
|
||||
Galaxy `cef.log` at 2.7 GB is NOT ours.)
|
||||
|
||||
## Tests
|
||||
- Last full run: **435/435 passed, 0 warnings** (2026-10-21, this session — after TASK 63's
|
||||
`BorderColorPickerTests`, which replaced TASK 62's `BorderColorNameTests`; the run before was also
|
||||
435/435 after TASK 61+62, and the runs before that were 433/433 after TASK 60, 432/432 after
|
||||
TASK 59 and 431/431 after TASK 58). The one failure seen earlier in the
|
||||
- Last full run: **436/436 passed, 0 warnings** (2026-10-22 — after TASK 55 slice 1's
|
||||
`AppLogRotationTests`; the run before was 435/435 after TASK 63, and the runs before that were
|
||||
435/435 after TASK 61+62, 433/433 after TASK 60, 432/432 after TASK 59 and 431/431 after
|
||||
TASK 58). The one failure seen earlier in the
|
||||
session was `LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder`,
|
||||
which **passes in isolation** (re-run: 3/3) — the known real-mouse flake, not a regression.
|
||||
- Known flaky-by-environment (all pass in isolation): `AudioPipelineTests.Mix_HonorsProviderGains…`,
|
||||
@@ -279,7 +288,8 @@ Zombie entities deleted from the registry: `script.leak_strobe`, `script.doorbel
|
||||
|
||||
## Secrets / paths (do not re-hunt)
|
||||
- Layout DB: `%APPDATA%\ytLlive\ytLlive.db` (SQLite, full-rewrite per save). Auth:
|
||||
`%APPDATA%\ytLlive\ytLlive.auth` (DPAPI). Startup log: `%APPDATA%\ytLlive\startup.log` (23MB).
|
||||
`%APPDATA%\ytLlive\ytLlive.auth` (DPAPI). Startup log: `%APPDATA%\ytLlive\startup.log`
|
||||
(23MB — now CAPPED at 5 MB with one rolled copy, TASK 55).
|
||||
- Recordings land in `C:\Users\gramp\Downloads\recordings\` (`ty-<date>-<time>-<ms>-<seq>.mp4`; a
|
||||
duplicate name gets a `-2` suffix).
|
||||
- **Conversation history:** `~/.local/share/opencode/opencode.db`, table **`session_message`** — creator
|
||||
|
||||
+19
-3
@@ -10,19 +10,35 @@ namespace ytLive.Helpers;
|
||||
/// </summary>
|
||||
public static class AppLog
|
||||
{
|
||||
private static readonly string LogPath = Path.Combine(
|
||||
/// <summary>The cap for the live log file. Past this, it rolls to <c>startup.1.log</c>.</summary>
|
||||
private const long DefaultMaxBytes = 5 * 1024 * 1024;
|
||||
|
||||
/// <summary>How many rolled copies to keep (startup.1.log, startup.2.log, …).</summary>
|
||||
private const int RetainedFiles = 1;
|
||||
|
||||
private static readonly string DefaultLogPath = Path.Combine(
|
||||
InstanceProfile.DataRoot, "startup.log");
|
||||
|
||||
private static readonly object Gate = new();
|
||||
|
||||
/// <summary>Test seam: point the log somewhere else. Null = the real profile's log.</summary>
|
||||
internal static string? PathOverride { get; set; }
|
||||
|
||||
/// <summary>Test seam: the cap that triggers a roll.</summary>
|
||||
internal static long MaxBytes { get; set; } = DefaultMaxBytes;
|
||||
|
||||
private static string LogPath => PathOverride ?? DefaultLogPath;
|
||||
|
||||
public static void Write(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(LogPath)!);
|
||||
File.AppendAllText(LogPath, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {message}{Environment.NewLine}");
|
||||
var path = LogPath;
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
LogRotation.RollIfNeeded(path, MaxBytes, RetainedFiles);
|
||||
File.AppendAllText(path, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {message}{Environment.NewLine}");
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.IO;
|
||||
|
||||
namespace ytLive.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Size-capped log rotation.
|
||||
/// <para>When a log file grows past a cap, it is rolled to <c><name>.1.log</c> and a
|
||||
/// fresh one is started, keeping a bounded number of rolled files. The pattern is the
|
||||
/// one every sized logger uses — Serilog's file sink pairs <c>fileSizeLimitBytes</c>
|
||||
/// (the cap) with <c>retainedFileCountLimit</c> (how many rolled files to keep):
|
||||
/// https://github.com/serilog/serilog-sinks-file</para>
|
||||
/// <para><b>Why it lives here and not in the logger:</b> <see cref="AppLog"/>'s call surface
|
||||
/// (156 call sites) must not change, and the rotation rule is worth testing on its own —
|
||||
/// so the policy is a pure function over a path, and <see cref="AppLog"/> just calls it.</para>
|
||||
/// </summary>
|
||||
public static class LogRotation
|
||||
{
|
||||
/// <summary>
|
||||
/// Rolls <paramref name="logPath"/> if it has reached <paramref name="maxBytes"/>,
|
||||
/// keeping <paramref name="retainedFiles"/> rolled copies
|
||||
/// (<c>name.1.log</c>, <c>name.2.log</c>, …). Does nothing when the file is missing
|
||||
/// or still under the cap.
|
||||
/// <para>Never throws: a failed rotation must not take the app down — the log just
|
||||
/// keeps growing, which is the old behaviour.</para>
|
||||
/// </summary>
|
||||
public static void RollIfNeeded(string logPath, long maxBytes, int retainedFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = new FileInfo(logPath);
|
||||
if (!info.Exists || info.Length < maxBytes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Oldest first: name.1 → name.2 → … → name.N, then the live file → name.1.
|
||||
for (var i = retainedFiles; i >= 1; i--)
|
||||
{
|
||||
var source = i == 1 ? logPath : RolledPath(logPath, i - 1);
|
||||
var target = RolledPath(logPath, i);
|
||||
if (!File.Exists(source))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
File.Move(source, target, overwrite: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Never let logging itself crash the app.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The rolled path for a given index: <c>startup.log</c> → <c>startup.1.log</c>.</summary>
|
||||
public static string RolledPath(string logPath, int index)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(logPath) ?? string.Empty;
|
||||
var name = Path.GetFileNameWithoutExtension(logPath);
|
||||
var extension = Path.GetExtension(logPath);
|
||||
return Path.Combine(directory, $"{name}.{index}{extension}");
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -6,7 +6,8 @@ Cross-cutting utilities. See [`schema.md`](../schema.md) for the memory-map conv
|
||||
|------|---------|
|
||||
| `ViewModelBase.cs` | `INotifyPropertyChanged` + `SetProperty<T>()` — base for all ViewModels |
|
||||
| `RelayCommand.cs` | `ICommand` implementation for all button actions |
|
||||
| `AppLog.cs` | File logger to `%APPDATA%\ytLlive\startup.log`; startup checkpoints + unhandled-exception capture (the crash this map is named for) |
|
||||
| `AppLog.cs` | File logger to `%APPDATA%\ytLlive\startup.log`; startup checkpoints + unhandled-exception capture (the crash this map is named for). Size-capped: rolls to `startup.1.log` at 5 MB (TASK 55) |
|
||||
| `LogRotation.cs` | The size-capped-rotation policy as a pure function over a path: roll `startup.log` → `startup.1.log` when it passes the cap, keeping N rolled copies. Separate from `AppLog` so the logger's 156-call-site surface never changes and the rule is testable alone (TASK 55) |
|
||||
| `FocusPreservingListBox.cs` | `ListBox` subclass that keeps selection/focus coherent when the selected item is deleted; `IsSelectable` predicate skips hidden scenes; drag-reorder guard |
|
||||
| `OAuthCredentials.cs` | Baked-in Google OAuth client ID/secret (desktop app; loopback callback) |
|
||||
| `TokenStore.cs` | DPAPI-protected OAuth session persistence (`%APPDATA%\ytLlive\ytLlive.auth`, CurrentUser scope); `Save`/`Load`/`Clear` — sign-in survives restarts |
|
||||
|
||||
@@ -2298,3 +2298,25 @@ so the honest report was "4 hits, all false positives", not "4 leaks".
|
||||
**Never print a matched value — but still say WHAT it is.** The report format that satisfies both:
|
||||
commit + path + line + rule + verdict. "README.md:79 | assigned-credential | placeholder" is
|
||||
actionable; the same report without the verdict forces the reader to go look at the secret.
|
||||
|
||||
## 2026-10-22 — CAP A LOGGER'S GROWTH WITHOUT TOUCHING ITS CALL SITES (RECIPE)
|
||||
|
||||
`AppLog` has **156 call sites** and one job; its log had no size cap (23 MB, growing every few
|
||||
seconds — ffmpeg logs every stderr line). The instinct is to add a cap parameter to the logger
|
||||
and change the callers. Don't.
|
||||
|
||||
**The recipe:**
|
||||
1. **Put the POLICY in a separate pure function over the path**, not in the logger:
|
||||
`LogRotation.RollIfNeeded(logPath, maxBytes, retainedFiles)`. The logger calls it before each
|
||||
append; its public surface (`AppLog.Write`) never changes, so no caller is touched.
|
||||
2. **Bound the FILE SET, not just the live file.** Rolling the live file alone (`startup.log` →
|
||||
`startup.1.log`) still grows forever if the rolled copy is never discarded. Shift the set:
|
||||
`name.1.log` → `name.2.log` → … → `name.N.log`, then the live file → `name.1.log`. Keep the
|
||||
rolled count explicit (`retainedFiles`) and assert the directory's file list in the test —
|
||||
that assertion is the one that proves the growth is bounded.
|
||||
3. **Test the logger, not just the helper** — otherwise the helper can be perfect and unwired.
|
||||
Two `internal` seams on the logger (`PathOverride`, `MaxBytes`) + the project's existing
|
||||
`InternalsVisibleTo("ytLive.Tests")` let the test drive the real `AppLog.Write` against a
|
||||
temp directory. Never let the test write to the real `%APPDATA%` log.
|
||||
4. **Never throw from the rotation**: a failed rotation must degrade to the old behaviour
|
||||
(the log keeps growing), never take the app down. Same rule as the logger itself.
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
| 09 | YouTube Live Stream Management | ⏳ In progress — items 1–3, 4 shipped; item 5 open; item 6 locked (TASK 36) | [`TASKS/task-09-live-stream-management.md`](TASKS/task-09-live-stream-management.md) |
|
||||
| 10 | Monetization: watermark-only paid delta (branding flash) | 🔶 **TEARDOWN, not a build** — Polar deleted in favour of Store IAP (2026-09-27); `$10/mo` + `$400` lifetime, no annual. **SUPERSEDED 2026-10-08** — free-forever ruling (watermark-free, gratuity/merch only); see TASK 54 | [`TASKS/task-10-monetization.md`](TASKS/task-10-monetization.md) |
|
||||
| 11 | Post-pause polish batch (creator's 8 review issues) | ✅ SHIPPED 2026-08-15 | [`TASKS/task-11-polish-batch.md`](TASKS/task-11-polish-batch.md) |
|
||||
| 12 | Master limiter on the live mix | ☐ Queued | [`TASKS/task-12-master-limiter.md`](TASKS/task-12-master-limiter.md) |
|
||||
| 12 | Master limiter on the live mix | ✅ Shipped 2026-08-15 — `Services/Audio/MasterLimiter.cs` (−1 dBFS ceiling, instant attack, smoothed release) applied at the end of `AudioMixer.FillAndMix` | [`TASKS/task-12-master-limiter.md`](TASKS/task-12-master-limiter.md) |
|
||||
| 13 | Social media launch kit | 🔶 Scoped — queued after v1 | [`TASKS/task-13-social-launch-kit.md`](TASKS/task-13-social-launch-kit.md) |
|
||||
| 14 | Creator feedback batch | 🔶 In progress — Branch 2 shipped | [`TASKS/task-14-creator-feedback-batch.md`](TASKS/task-14-creator-feedback-batch.md) |
|
||||
| 15 | Stock scene background images | ✅ Shipped | [`TASKS/task-15-scene-backgrounds.md`](TASKS/task-15-scene-backgrounds.md) |
|
||||
@@ -70,7 +70,7 @@
|
||||
| 52 | **Zoom box (anchored 2× magnifier)** | ✅ Done — shipped 2026-10-08 (3 creator corrections during the build: loupe → anchored capture over the full composite, drawn last → never shows the dim shading) | [`TASKS/task-52-zoom-box.md`](TASKS/task-52-zoom-box.md) |
|
||||
| 53 | **Output-format verification: Shorts/TikTok geometry (9:16)** | ☐ Queued (2026-10-08) — output formats never tested end-to-end; pin + prove the vertical geometry | [`TASKS/task-53-output-formats.md`](TASKS/task-53-output-formats.md) |
|
||||
| 54 | **Marcom round: free-forever positioning + merch storefront** | 🔶 In progress — llamacasty.com copy live 2026-10-08 (pricing stripped); merch POD + app About links queued | [`TASKS/task-54-marcom-free-merch.md`](TASKS/task-54-marcom-free-merch.md) |
|
||||
| 55 | **Disk hygiene: log rotation + low-disk guard** | ☐ Queued (2026-10-09) — `startup.log` appears uncapped; also temp alert-clip cleanup + free-space check before recording | [`TASKS/task-55-disk-hygiene.md`](TASKS/task-55-disk-hygiene.md) |
|
||||
| 55 | **Disk hygiene: log rotation + low-disk guard** | 🔶 In progress — **slice 1 shipped 2026-10-22** (`startup.log` rolls at 5 MB to `startup.1.log`, one copy kept — it was uncapped at 23 MB and growing every few seconds); slices 2 (temp alert-clip cleanup) + 3 (low-disk guard) open | [`TASKS/task-55-disk-hygiene.md`](TASKS/task-55-disk-hygiene.md) |
|
||||
| 56 | **Pre-publication secret scan (full git history)** | ✅ Done — 2026-10-22, **CLEAN** (432 reachable commits + 63 dangling + worktree; 4 hits, all false positives: a `your-client-secret` doc placeholder and a test dummy — nothing to rotate). ⚠️ the repo was **already public** when the scan ran (anonymous GET → 200), so the gate came late — but nothing was exposed | [`TASKS/task-56-secret-scan.md`](TASKS/task-56-secret-scan.md) |
|
||||
| 57 | **Vertical (9:16) tier camera placement — the "vv" feature** | ✅ **Both units shipped 2026-10-21** (Unit 1 = Live scene: own placement per tier — small tile, circle-or-rect, one of six spots; drag-snap + manual entry; 16:9 restored exactly. Unit 2 = Chat scene: rect only — the circle is not offered — the tile or one whole 607×540 area) | [`TASKS/task-57-vertical-camera-layout.md`](TASKS/task-57-vertical-camera-layout.md) |
|
||||
| 58 | **Properties-panel style consistency (the theme owns the look)** | ✅ Done — 2026-10-21 (the creator's "why is the drop-down not in the same style as the others?" — the drop-downs/fields/check boxes had been hand-painted instead of themed, so WPF drew the light system template; now every one carries a theme style, guarded by a test) | [`TASKS/task-58-properties-style-consistency.md`](TASKS/task-58-properties-style-consistency.md) |
|
||||
|
||||
@@ -17,10 +17,19 @@ ticket and a trust breach.
|
||||
|
||||
**Slices (one integration test each, in order):**
|
||||
|
||||
1. ☐ **Size-capped log rotation** — when `startup.log` passes a cap (~5 MB), roll it to
|
||||
1. ✅ **Size-capped log rotation** — when `startup.log` passes a cap (~5 MB), roll it to
|
||||
`startup.1.log` (keep 1–2 old files), start fresh. Test: write past the cap, assert the roll
|
||||
and the bounded total. Standard pattern (NLog/Serilog `fileSizeLimitBytes` +
|
||||
`retainedFileCountLimit`) — cite in the commit.
|
||||
- **SHIPPED 2026-10-22.** `Helpers/LogRotation.cs` (new) holds the policy as a pure function
|
||||
over a path — `RollIfNeeded(logPath, maxBytes, retainedFiles)` shifts `name.1.log` →
|
||||
`name.2.log` → … and the live file → `name.1.log`, so the **file set** is bounded, not just
|
||||
the live file. `AppLog` calls it before each append (cap **5 MB**, **1** rolled copy) and
|
||||
keeps its 156-call-site surface unchanged; the test seams are two `internal` properties
|
||||
(`PathOverride`, `MaxBytes`) — the app project already has `InternalsVisibleTo("ytLive.Tests")`.
|
||||
Guard: `ytLive.Tests/AppLogRotationTests.cs` (the ONE test: the log rolls at the cap, the
|
||||
live file starts fresh, and the directory still holds exactly `startup.log` + `startup.1.log`
|
||||
after a SECOND roll — the growth is bounded).
|
||||
2. ☐ **Temp alert clip cleanup** — delete the `%TEMP%` alert clip on app close (and sweep
|
||||
stale ones at startup). Test: create, close, assert gone.
|
||||
3. ☐ **Low-disk guard for recordings** — check free space on the record folder's drive before
|
||||
|
||||
@@ -319,7 +319,7 @@ C# / WPF (.NET 8) following MVVM:
|
||||
`FormattedText` — **measure, don't guess**) plus the field's padding and border. Guard:
|
||||
`ytLive.Tests/GeometryFieldWidthTests.cs`
|
||||
- Resolution tiers: 1080p60@8 (default) → 1080p30@8 → 720p60@6 → 720p30@6 → **Vertical 1080p60@8 (9:16, 1080×1920)**. The composition master frame is **always 1920×1080** — a tier is an output rect + target resolution over that master, so source geometry is never rewritten (no rounding drift). 16:9 tiers use the full frame; the vertical tier uses a centered **607×1080** window and the preview dims the cropped side strips at 55% black with an accent outline (semi-crop — the cut area stays visible). A resolution badge in the preview corner shows the active tier. Bottom bar: gear icon far left, stream stats (bitrate/FPS/dropped/duration) centered under preview, resolution dropdown far right. A **tooltip** explains finding upload bandwidth — an in-app speed test was deliberately dropped (unreliable). The future encoder crops the master to the rect and scales to the tier's Width×Height
|
||||
- Crash diagnosis: `AppLog` writes startup checkpoints to `%APPDATA%\ytLlive\startup.log`; `App.xaml.cs` logs `DispatcherUnhandledException`/`AppDomain.UnhandledException`. When WPF won't run from WSL, this log is how you find the failure (it caught the `MenuItemRole.Separator` XAML crash and the ComboBox SelectionBoxItem bug)
|
||||
- Crash diagnosis: `AppLog` writes startup checkpoints to `%APPDATA%\ytLlive\startup.log`; `App.xaml.cs` logs `DispatcherUnhandledException`/`AppDomain.UnhandledException`. When WPF won't run from WSL, this log is how you find the failure (it caught the `MenuItemRole.Separator` XAML crash and the ComboBox SelectionBoxItem bug). **The log is SIZE-CAPPED (TASK 55, 2026-10-22):** at 5 MB `startup.log` rolls to `startup.1.log` (one rolled copy kept) — the policy lives in `Helpers/LogRotation.cs` as a pure function over a path, so `AppLog`'s 156-call-site surface never changes and the rule is testable on its own. Guard: `ytLive.Tests/AppLogRotationTests.cs`
|
||||
|
||||
### ⚠️ Known issue: screens/layers settings audit — RETIRED INTO THE GOLD PASS (2026-08-22, dispositioned 2026-09-01)
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
using ytLive.Helpers;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>TASK 55, slice 1 — the creator (2026-10-09): *"we need some sort of log-rotation
|
||||
/// so that we don't inadvertently fill up a user's drive."*
|
||||
///
|
||||
/// <para>One test: the app's log is size-capped. Past the cap the file rolls to
|
||||
/// <c>startup.1.log</c>, the live file starts fresh with the newest entry, and the SET of
|
||||
/// files stays bounded — a long stream cannot grow the log without limit. (The old
|
||||
/// behaviour appended forever; the file was 23 MB and growing every few seconds.)</para></summary>
|
||||
public class AppLogRotationTests
|
||||
{
|
||||
[Fact]
|
||||
public void The_startup_log_rolls_at_its_cap_and_the_file_set_stays_bounded()
|
||||
{
|
||||
var directory = Path.Combine(Path.GetTempPath(),
|
||||
"ytlive-log-rotation-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(directory);
|
||||
var live = Path.Combine(directory, "startup.log");
|
||||
var rolled = Path.Combine(directory, "startup.1.log");
|
||||
const long cap = 4096;
|
||||
var first = "first-" + new string('x', 3000);
|
||||
var second = "second-" + new string('y', 3000);
|
||||
var third = "third-" + new string('z', 3000);
|
||||
|
||||
try
|
||||
{
|
||||
AppLog.PathOverride = live;
|
||||
AppLog.MaxBytes = cap;
|
||||
|
||||
AppLog.Write(first);
|
||||
AppLog.Write(second);
|
||||
Assert.False(File.Exists(rolled)); // under the cap — nothing rolls yet
|
||||
|
||||
AppLog.Write(third); // this write trips the cap
|
||||
Assert.True(File.Exists(rolled));
|
||||
Assert.Contains(first, File.ReadAllText(rolled));
|
||||
Assert.Contains(second, File.ReadAllText(rolled));
|
||||
Assert.DoesNotContain(first, File.ReadAllText(live)); // the live file is fresh
|
||||
Assert.Contains(third, File.ReadAllText(live));
|
||||
Assert.True(new FileInfo(live).Length < cap);
|
||||
|
||||
// A second roll must not grow the file set: the live file plus one rolled copy.
|
||||
AppLog.Write(first);
|
||||
AppLog.Write(second);
|
||||
AppLog.Write(third);
|
||||
|
||||
var files = Directory.GetFiles(directory)
|
||||
.Select(Path.GetFileName)
|
||||
.OrderBy(name => name, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
Assert.Equal(new[] { "startup.1.log", "startup.log" }, files);
|
||||
}
|
||||
finally
|
||||
{
|
||||
AppLog.PathOverride = null;
|
||||
AppLog.MaxBytes = 5 * 1024 * 1024;
|
||||
try { Directory.Delete(directory, true); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user