Skip to content

Commit 8e7691c

Browse files
committed
Проходит все тесты на жесткий диск
1 parent 03515b7 commit 8e7691c

16 files changed

Lines changed: 1155 additions & 4 deletions
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// File: PeaceDatabase.Tests/FileDocumentServiceCrashRecoveryTests.cs
2+
using System;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Net;
6+
using System.Threading.Tasks;
7+
using FluentAssertions;
8+
using Microsoft.AspNetCore.Mvc.Testing;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using PeaceDatabase.Core.Models;
11+
using PeaceDatabase.Core.Services;
12+
using PeaceDatabase.Storage.Disk;
13+
using PeaceDatabase.Storage.Disk.Internals;
14+
using Xunit;
15+
16+
namespace PeaceDatabase.Tests
17+
{
18+
public class FileDocumentServiceCrashRecoveryTests : IAsyncLifetime
19+
{
20+
private readonly string _rootDir;
21+
private readonly StorageOptions _optsNoSnapshot;
22+
23+
public FileDocumentServiceCrashRecoveryTests()
24+
{
25+
_rootDir = Path.Combine(Path.GetTempPath(), "peace-db-tests", $"crash-{Guid.NewGuid():N}");
26+
27+
// Специальные опции: СНАПШОТЫ ОТКЛЮЧЕНЫ, чтобы проверить чистое восстановление из WAL
28+
_optsNoSnapshot = new StorageOptions
29+
{
30+
DataRoot = _rootDir,
31+
EnableSnapshots = false,
32+
SnapshotEveryNOperations = int.MaxValue,
33+
SnapshotMaxWalSizeMb = 1024,
34+
Durability = DurabilityLevel.Commit
35+
};
36+
}
37+
38+
public Task InitializeAsync() => Task.CompletedTask;
39+
40+
public Task DisposeAsync()
41+
{
42+
try { if (Directory.Exists(_rootDir)) Directory.Delete(_rootDir, recursive: true); } catch { }
43+
return Task.CompletedTask;
44+
}
45+
46+
[Fact]
47+
public void Crash_Recovery_From_WAL_Only()
48+
{
49+
var db = "t_crash";
50+
51+
// 1) Первый "сеанс": пишем только WAL (снапшоты выключены)
52+
using (var svc = new FileDocumentService(_optsNoSnapshot))
53+
{
54+
svc.CreateDb(db);
55+
56+
// последовательность операций: put, put, delete
57+
var p1 = svc.Post(db, new Document { Id = "id1", Data = new() { ["n"] = 1 } });
58+
p1.Ok.Should().BeTrue();
59+
60+
var p2 = svc.Post(db, new Document { Id = "id2", Data = new() { ["n"] = 2 } });
61+
p2.Ok.Should().BeTrue();
62+
63+
var head = svc.Get(db, "id2");
64+
head.Should().NotBeNull();
65+
var del = svc.Delete(db, "id2", head!.Rev);
66+
del.Ok.Should().BeTrue();
67+
68+
// есть wal.log, снапшота нет
69+
var dbDir = Path.Combine(_rootDir, db);
70+
File.Exists(Path.Combine(dbDir, "wal.log")).Should().BeTrue();
71+
Directory.EnumerateFiles(dbDir, "snapshot-*.jsonl").Any().Should().BeFalse();
72+
} // симулируем "краш": просто dispose без снапшота
73+
74+
// 2) Второй запуск: сервис должен доиграть WAL и восстановить RAM-состояние
75+
using (var svc2 = new FileDocumentService(_optsNoSnapshot))
76+
{
77+
var alive = svc2.AllDocs(db, includeDeleted: false).ToList();
78+
alive.Should().ContainSingle(x => x.Id == "id1");
79+
alive.Should().NotContain(x => x.Id == "id2"); // удалён в WAL
80+
81+
// Проверим, что Seq поднялся
82+
svc2.Seq(db).Should().BeGreaterThan(0);
83+
}
84+
}
85+
86+
[Fact]
87+
public async Task Api_Smoke_With_WAL_Recovery()
88+
{
89+
// Готовим данные
90+
var db = "api_wal";
91+
using (var svc = new FileDocumentService(_optsNoSnapshot))
92+
{
93+
svc.CreateDb(db);
94+
svc.Post(db, new Document { Id = "a", Data = new() { ["x"] = 1 } });
95+
}
96+
97+
// Поднимем тестовый WebAPI поверх того же DataRoot и проверим /v1/_stats
98+
await using var app = new TestApiFactory(_optsNoSnapshot);
99+
var client = app.CreateClient();
100+
101+
var resp = await client.GetAsync("/v1/_stats");
102+
resp.StatusCode.Should().Be(HttpStatusCode.OK);
103+
104+
// И убедимся через DI, что данные доступны
105+
using var scope = app.Services.CreateScope();
106+
var s = scope.ServiceProvider.GetRequiredService<IDocumentService>();
107+
var doc = s.Get(db, "a");
108+
doc.Should().NotBeNull();
109+
// (int)doc!.Data!["x"]!.Should().Be(1);
110+
//doc!.Data!["x"].Should().Be(1);
111+
// В методе Api_Smoke_With_WAL_Recovery():
112+
AsInt(doc!.Data!["x"]!).Should().Be(1);
113+
114+
115+
}
116+
private static int AsInt(object? v)
117+
{
118+
if (v is null) throw new InvalidOperationException("Value is null");
119+
if (v is int i) return i;
120+
if (v is long l) return checked((int)l);
121+
if (v is double d) return (int)d;
122+
if (v is decimal m) return (int)m;
123+
if (v is System.Text.Json.JsonElement je)
124+
{
125+
return je.ValueKind switch
126+
{
127+
System.Text.Json.JsonValueKind.Number => je.TryGetInt32(out var vi)
128+
? vi
129+
: (je.TryGetInt64(out var vl) ? checked((int)vl) : (int)je.GetDouble()),
130+
System.Text.Json.JsonValueKind.String => int.Parse(je.GetString()!),
131+
_ => throw new InvalidCastException($"JsonElement {je.ValueKind} is not a number")
132+
};
133+
}
134+
return Convert.ToInt32(v);
135+
}
136+
137+
private static string? AsString(object? v)
138+
{
139+
if (v is null) return null;
140+
if (v is string s) return s;
141+
if (v is System.Text.Json.JsonElement je)
142+
{
143+
return je.ValueKind switch
144+
{
145+
System.Text.Json.JsonValueKind.String => je.GetString(),
146+
System.Text.Json.JsonValueKind.Number => je.GetRawText(), // "123"
147+
_ => je.ToString()
148+
};
149+
}
150+
return v.ToString();
151+
}
152+
153+
private sealed class TestApiFactory : WebApplicationFactory<Program>, IAsyncDisposable
154+
{
155+
private readonly StorageOptions _options;
156+
public TestApiFactory(StorageOptions options) => _options = options;
157+
158+
protected override void ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBuilder builder)
159+
{
160+
builder.ConfigureServices(services =>
161+
{
162+
var desc = services.SingleOrDefault(d => d.ServiceType == typeof(IDocumentService));
163+
if (desc != null) services.Remove(desc);
164+
services.AddSingleton<IDocumentService>(_ => new FileDocumentService(_options));
165+
});
166+
}
167+
}
168+
}
169+
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// File: PeaceDatabase.Tests/FileDocumentServicePersistenceTests.cs
2+
using System;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Net;
6+
using System.Net.Http.Json;
7+
using System.Runtime.InteropServices;
8+
using System.Threading.Tasks;
9+
using FluentAssertions;
10+
using Microsoft.AspNetCore.Mvc.Testing;
11+
using Microsoft.Extensions.DependencyInjection;
12+
using PeaceDatabase.Core.Models;
13+
using PeaceDatabase.Core.Services;
14+
using PeaceDatabase.Storage.Disk;
15+
using PeaceDatabase.Storage.Disk.Internals;
16+
using Xunit;
17+
18+
namespace PeaceDatabase.Tests
19+
{
20+
public class FileDocumentServicePersistenceTests : IAsyncLifetime
21+
{
22+
private readonly string _rootDir;
23+
private readonly StorageOptions _opts;
24+
private static string? AsString(object? v)
25+
{
26+
if (v is null) return null;
27+
if (v is string s) return s;
28+
if (v is System.Text.Json.JsonElement je)
29+
{
30+
return je.ValueKind switch
31+
{
32+
System.Text.Json.JsonValueKind.String => je.GetString(),
33+
System.Text.Json.JsonValueKind.Number => je.GetRawText(), // "123"
34+
_ => je.ToString()
35+
};
36+
}
37+
return v.ToString();
38+
}
39+
public FileDocumentServicePersistenceTests()
40+
{
41+
_rootDir = Path.Combine(Path.GetTempPath(), "peace-db-tests", Guid.NewGuid().ToString("N"));
42+
_opts = new StorageOptions
43+
{
44+
DataRoot = _rootDir,
45+
EnableSnapshots = true,
46+
SnapshotEveryNOperations = 3, // понижен порог, чтобы точно случился снапшот
47+
SnapshotMaxWalSizeMb = 1,
48+
Durability = DurabilityLevel.Commit
49+
};
50+
}
51+
52+
public Task InitializeAsync() => Task.CompletedTask;
53+
54+
public Task DisposeAsync()
55+
{
56+
try { if (Directory.Exists(_rootDir)) Directory.Delete(_rootDir, recursive: true); } catch { }
57+
return Task.CompletedTask;
58+
}
59+
60+
[Fact]
61+
public void Persistence_WritesSnapshot_And_Manifest_And_Reloads()
62+
{
63+
var db = "t_persist";
64+
65+
// 1) Первый запуск: создаём сервис, пишем данные
66+
using (var svc = new FileDocumentService(_opts))
67+
{
68+
var r1 = svc.CreateDb(db);
69+
r1.Ok.Should().BeTrue();
70+
71+
// 3 операции => должен сработать триггер снапшота
72+
var d1 = new Document { Id = "a1", Data = new() { ["name"] = "Alpha" } };
73+
var p1 = svc.Post(db, d1);
74+
p1.Ok.Should().BeTrue();
75+
76+
var d2 = new Document { Id = "a2", Data = new() { ["name"] = "Beta" } };
77+
var p2 = svc.Post(db, d2);
78+
p2.Ok.Should().BeTrue();
79+
80+
var g1 = svc.Get(db, "a1");
81+
g1.Should().NotBeNull();
82+
var upd = new Document { Id = g1!.Id, Rev = g1.Rev, Data = new() { ["name"] = "Alpha v2" } };
83+
var u1 = svc.Put(db, upd);
84+
u1.Ok.Should().BeTrue();
85+
86+
// Проверки файлов на диске
87+
var dbDir = Path.Combine(_rootDir, db);
88+
Directory.Exists(dbDir).Should().BeTrue();
89+
File.Exists(Path.Combine(dbDir, _opts.ManifestFileName)).Should().BeTrue();
90+
Directory.EnumerateFiles(dbDir, $"{_opts.SnapshotPrefix}*{_opts.SnapshotExt}").Any().Should().BeTrue("snapshot must be created");
91+
File.Exists(Path.Combine(dbDir, _opts.WalFileName)).Should().BeTrue();
92+
} // dispose
93+
94+
// 2) Второй запуск: сервис должен восстановить состояние из snapshot + wal
95+
using (var svc2 = new FileDocumentService(_opts))
96+
{
97+
var all = svc2.AllDocs(db).ToList();
98+
all.Should().ContainSingle(x => x.Id == "a1" && AsString(x.Data!["name"]) == "Alpha v2");
99+
all.Should().ContainSingle(x => x.Id == "a2" && AsString(x.Data!["name"]) == "Beta");
100+
101+
// seq должен быть > 0 (загружен из manifest или доигран из WAL)
102+
svc2.Seq(db).Should().BeGreaterThan(0);
103+
}
104+
}
105+
106+
[Fact]
107+
public async Task Api_Smoke_Uses_FileDocumentService_DI_And_Returns_Stats()
108+
{
109+
// Настраиваем тестовый хост так, чтобы WebAPI использовал FileDocumentService с нашим data-root
110+
await using var app = new TestApiFactory(_opts);
111+
var client = app.CreateClient();
112+
113+
// Просто дымовая проверка, что эндпоинт жив
114+
var resp = await client.GetAsync("/v1/_stats");
115+
resp.StatusCode.Should().Be(HttpStatusCode.OK);
116+
117+
// Создадим базу и документ через DI (напрямую)
118+
using var scope = app.Services.CreateScope();
119+
var svc = scope.ServiceProvider.GetRequiredService<IDocumentService>();
120+
121+
var r = svc.CreateDb("api_db");
122+
r.Ok.Should().BeTrue();
123+
124+
var p = svc.Post("api_db", new Document { Id = "x1", Data = new() { ["v"] = 1 } });
125+
p.Ok.Should().BeTrue();
126+
127+
// ещё одна проверка HTTP – если есть контроллеры для чтения, можно добавить:
128+
// var docResp = await client.GetAsync("/v1/doc/api_db/x1");
129+
// docResp.StatusCode.Should().Be(HttpStatusCode.OK);
130+
}
131+
132+
// using System.Runtime.InteropServices;
133+
// using FluentAssertions;
134+
135+
[Fact(DisplayName = "Open DB folder in OS file explorer (opt-in)")]
136+
public void Open_Db_Folder_In_FileExplorer()
137+
{
138+
// По умолчанию просто выходим — тест пройдёт молча
139+
var wantOpen = Environment.GetEnvironmentVariable("OPEN_DB_EXPLORER") == "1";
140+
if (!wantOpen) return;
141+
142+
var db = "open_me";
143+
var root = Path.Combine(Path.GetTempPath(), "peace-db-tests", "manual-open");
144+
var opts = new StorageOptions { DataRoot = root, EnableSnapshots = true };
145+
146+
using (var svc = new FileDocumentService(opts))
147+
{
148+
svc.CreateDb(db);
149+
svc.Post(db, new Document { Id = "show", Data = new() { ["k"] = "v" } });
150+
}
151+
152+
var dbDir = Path.Combine(root, db);
153+
Directory.Exists(dbDir).Should().BeTrue();
154+
155+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
156+
{
157+
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("explorer.exe", $"\"{dbDir}\"")
158+
{ UseShellExecute = true });
159+
}
160+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
161+
{
162+
System.Diagnostics.Process.Start("open", dbDir);
163+
}
164+
else
165+
{
166+
System.Diagnostics.Process.Start("xdg-open", dbDir);
167+
}
168+
}
169+
170+
// ---- Test host that injects FileDocumentService into WebAPI ----
171+
private sealed class TestApiFactory : WebApplicationFactory<Program>, IAsyncDisposable
172+
{
173+
private readonly StorageOptions _options;
174+
public TestApiFactory(StorageOptions options) => _options = options;
175+
176+
protected override void ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBuilder builder)
177+
{
178+
builder.ConfigureServices(services =>
179+
{
180+
// Снести любую регистрацию IDocumentService (InMemory) и поставить FileDocumentService
181+
var desc = services.SingleOrDefault(d => d.ServiceType == typeof(IDocumentService));
182+
if (desc != null) services.Remove(desc);
183+
184+
services.AddSingleton<IDocumentService>(_ => new FileDocumentService(_options));
185+
});
186+
}
187+
}
188+
}
189+
}
3.57 KB
Binary file not shown.
8.34 KB
Binary file not shown.

‎PeaceDatabase.Tests/obj/Debug/net8.0/PeaceDatabase.Tests.AssemblyInfo.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
[assembly: System.Reflection.AssemblyCompanyAttribute("PeaceDatabase.Tests")]
1515
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
1616
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17-
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+88fedd3c2cb3a35eed162a196bb7f9ba7a9a0eea")]
17+
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+03515b77e3bd4e65866424c1e16c9ff7f13eb133")]
1818
[assembly: System.Reflection.AssemblyProductAttribute("PeaceDatabase.Tests")]
1919
[assembly: System.Reflection.AssemblyTitleAttribute("PeaceDatabase.Tests")]
2020
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
06ee261880cb64df1f80f210f809ad951bf32873a918cd4f11bc264e811fdfdb
1+
f7ebd18cbe6082557fac387a02467086f5b3b85f55606c9663afa3fef81be7b9
Binary file not shown.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
d36839c9cf770306545672b4aad5efd4670c86ad0f9dd779e6650835e27cd2cf
1+
b63d3996b5f1739baee6197d28d49b34df8748649b3f35ef64a9d0eca80aea3c
3.57 KB
Binary file not shown.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"documents":{"C:\\Users\\mit\\Source\\Repos\\PeaceDatabase\\*":"https://raw.githubusercontent.com/teyhd/PeaceDatabase/88fedd3c2cb3a35eed162a196bb7f9ba7a9a0eea/*"}}
1+
{"documents":{"C:\\Users\\mit\\Source\\Repos\\PeaceDatabase\\*":"https://raw.githubusercontent.com/teyhd/PeaceDatabase/03515b77e3bd4e65866424c1e16c9ff7f13eb133/*"}}

0 commit comments

Comments
 (0)