-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathSafeWriteTest.bb
More file actions
167 lines (133 loc) · 5.49 KB
/
SafeWriteTest.bb
File metadata and controls
167 lines (133 loc) · 5.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
Strict
EnableGC
; Exercise the real SafeWriteOpen / SafeWriteCommit / SafeWriteAbort in
; Logging.bb against the working directory's filesystem. WriteLog needs
; LogMode + MainLog globals to exist; default LogMode=0 keeps it quiet
; so no Data\Logs directory is required.
Global LogMode = 0
Global MainLog = 0
Include "Modules\Logging.bb"
Global testDir$ = CurrentDir$()
Global ProductionPath$ = testDir$ + "safewrite_test.dat"
Global TempPathExpected$ = ProductionPath$ + ".tmp"
Global BakPath$ = ProductionPath$ + ".bak"
Function CleanupTestFiles()
If FileType(ProductionPath$) = 1 Then DeleteFile(ProductionPath$)
If FileType(TempPathExpected$) = 1 Then DeleteFile(TempPathExpected$)
If FileType(BakPath$) = 1 Then DeleteFile(BakPath$)
End Function
; Helper: write payload to path and close the handle. We close locally
; (rather than letting SafeWriteCommit close) because Strict mode can't
; bridge a BBStream local to SafeWriteCommit's untyped Int parameter --
; passing F=0 tells the helper "I already closed it".
Function SeedFile(path$, payload$)
Local s.BBStream = WriteFile(path$)
If s = Null Then Return
WriteString(s, payload$)
CloseFile(s)
End Function
Function SeedEmptyFile(path$)
Local s.BBStream = WriteFile(path$)
If s = Null Then Return
CloseFile(s)
End Function
Function ReadFileString$(path$)
Local s.BBStream = ReadFile(path$)
If s = Null Then Return ""
Local payload$ = ReadString(s)
CloseFile(s)
Return payload$
End Function
; SafeWriteOpen is a pure helper: it just appends .tmp to the final path.
; Pin that contract -- callers rely on the temp name being deterministic
; so cleanup paths (SafeWriteAbort) can target it.
Test testSafeWriteOpenReturnsTempSuffixedPath()
Local temp$ = SafeWriteOpen$(ProductionPath$)
Assert(temp$ = ProductionPath$ + ".tmp")
End Test
; Happy path: write to temp, commit promotes the temp into production and
; deletes the temp. No prior production file exists, so no .bak is made.
Test testSafeWriteCommitPromotesTempToProductionOnFirstSave()
CleanupTestFiles()
Local temp$ = SafeWriteOpen$(ProductionPath$)
SeedFile(temp$, "first save payload")
Local ok% = SafeWriteCommit%(temp$, ProductionPath$, 0)
Assert(ok = True)
Assert(FileType(ProductionPath$) = 1)
Assert(FileType(temp$) <> 1) ; temp consumed
Assert(FileType(BakPath$) <> 1) ; no prior file -> no .bak
CleanupTestFiles()
End Test
; When a production file already exists, commit demotes it to .bak before
; promoting the new temp. Lets a crash mid-promote recover the previous
; version.
Test testSafeWriteCommitDemotesPriorProductionToBak()
CleanupTestFiles()
SeedFile(ProductionPath$, "previous save")
Local temp$ = SafeWriteOpen$(ProductionPath$)
SeedFile(temp$, "newer save")
Local ok% = SafeWriteCommit%(temp$, ProductionPath$, 0)
Assert(ok = True)
Assert(FileType(ProductionPath$) = 1)
Assert(FileType(BakPath$) = 1) ; previous version preserved
Assert(FileType(temp$) <> 1)
Assert(ReadFileString$(BakPath$) = "previous save")
Assert(ReadFileString$(ProductionPath$) = "newer save")
CleanupTestFiles()
End Test
; Empty temp = silent WriteFile failure. SafeWriteCommit must refuse to
; promote an empty file, otherwise production would be replaced with 0
; bytes (which is the whole bug the helper exists to prevent).
Test testSafeWriteCommitRefusesEmptyTemp()
CleanupTestFiles()
SeedFile(ProductionPath$, "must survive")
Local temp$ = SafeWriteOpen$(ProductionPath$)
SeedEmptyFile(temp$)
Local ok% = SafeWriteCommit%(temp$, ProductionPath$, 0)
Assert(ok = False)
Assert(FileType(ProductionPath$) = 1) ; production untouched
Assert(FileType(temp$) <> 1) ; empty temp deleted by commit
Assert(ReadFileString$(ProductionPath$) = "must survive")
CleanupTestFiles()
End Test
; Successive commits cycle the .bak: after three saves A -> B -> C,
; .bak must hold B (the immediately previous version), not A. This pins
; the behaviour at Logging.bb's `If FileType(Bak$) = 1 Then DeleteFile(Bak$)`
; line -- without that delete, the second CopyFile into an existing
; .bak target either fails silently or appends, and the .bak content
; diverges from the most-recent-pre-save state. The RC Terrain Editor
; SaveAreaTE migration (and the GUE SaveArea before it) saves on every
; build-cycle hotkey; an author hitting Save twice in a row must be able
; to recover the *previous* save, not the original empty area.
Test testSafeWriteCommitCyclesBakOnSuccessiveSaves()
CleanupTestFiles()
; Save A (no prior file -> no .bak)
Local tempA$ = SafeWriteOpen$(ProductionPath$)
SeedFile(tempA$, "save A")
Assert(SafeWriteCommit%(tempA$, ProductionPath$, 0) = True)
Assert(FileType(BakPath$) <> 1)
; Save B (A demoted to .bak)
Local tempB$ = SafeWriteOpen$(ProductionPath$)
SeedFile(tempB$, "save B")
Assert(SafeWriteCommit%(tempB$, ProductionPath$, 0) = True)
Assert(ReadFileString$(ProductionPath$) = "save B")
Assert(ReadFileString$(BakPath$) = "save A")
; Save C (B demoted to .bak, displacing A)
Local tempC$ = SafeWriteOpen$(ProductionPath$)
SeedFile(tempC$, "save C")
Assert(SafeWriteCommit%(tempC$, ProductionPath$, 0) = True)
Assert(ReadFileString$(ProductionPath$) = "save C")
Assert(ReadFileString$(BakPath$) = "save B")
CleanupTestFiles()
End Test
; SafeWriteAbort cleans up the temp when the caller decides not to commit
; (e.g. a serialization error mid-write).
Test testSafeWriteAbortRemovesTemp()
CleanupTestFiles()
Local temp$ = SafeWriteOpen$(ProductionPath$)
SeedFile(temp$, "abandoned")
Assert(FileType(temp$) = 1)
SafeWriteAbort(temp$)
Assert(FileType(temp$) <> 1)
CleanupTestFiles()
End Test