@@ -204,3 +204,72 @@ func TestSaveWritesOwnerOnlyPermissions(t *testing.T) {
204204 require .NoError (t , err )
205205 assert .Equal (t , os .FileMode (0o600 ), info .Mode ().Perm (), "saved test data must be owner read/write only" )
206206}
207+
208+ // nonStoppingT is a TestingT whose FailNow returns rather than calling runtime.Goexit, so these tests can assert
209+ // that the package stops doing work after it reports a failure.
210+ type nonStoppingT struct {
211+ failed bool
212+ msgs []string
213+ }
214+
215+ func (r * nonStoppingT ) Fail () { r .failed = true }
216+ func (r * nonStoppingT ) FailNow () { r .failed = true }
217+ func (r * nonStoppingT ) Error (args ... any ) { r .failed = true }
218+ func (r * nonStoppingT ) Errorf (string , ... any ) { r .failed = true }
219+ func (r * nonStoppingT ) Fatal (args ... any ) { r .msgs = append (r .msgs , fmt .Sprint (args ... )); r .FailNow () }
220+ func (r * nonStoppingT ) Name () string { return "nonStoppingT" }
221+ func (r * nonStoppingT ) Helper () {}
222+ func (r * nonStoppingT ) Fatalf (f string , a ... any ) {
223+ r .msgs = append (r .msgs , fmt .Sprintf (f , a ... ))
224+ r .FailNow ()
225+ }
226+
227+ // unmarshalable fails json.Marshal: encoding/json always rejects a func field.
228+ type unmarshalable struct {
229+ Fn func ()
230+ }
231+
232+ // Before the fix, a marshal failure was reported and os.WriteFile still ran with a nil slice, leaving a zero byte
233+ // file for a later stage to load.
234+ func TestSaveWritesNothingAfterAMarshalFailure (t * testing.T ) {
235+ t .Parallel ()
236+
237+ folder := t .TempDir ()
238+ path := teststate .FormatPath (folder , "Broken.json" )
239+
240+ recorder := & nonStoppingT {}
241+ teststate .Save (recorder , path , true , unmarshalable {})
242+
243+ assert .True (t , recorder .failed , "the marshal failure must be reported" )
244+ assert .NoFileExists (t , path , "no file may be written after a marshal failure" )
245+ }
246+
247+ // A read failure must be reported, not reported as "absent", which would invite a caller to overwrite state it
248+ // could not read.
249+ func TestIsPresentDoesNotMaskAnUnreadableFile (t * testing.T ) {
250+ t .Parallel ()
251+
252+ // A directory where a file is expected: FileExistsE succeeds, os.ReadFile fails with EISDIR.
253+ folder := t .TempDir ()
254+ path := teststate .FormatPath (folder , "IsADirectory.json" )
255+ require .NoError (t , os .MkdirAll (path , 0o755 ))
256+
257+ recorder := & nonStoppingT {}
258+ present := teststate .IsPresent (recorder , path )
259+
260+ assert .True (t , recorder .failed , "the read failure must be reported" )
261+ assert .False (t , present )
262+ require .NotEmpty (t , recorder .msgs )
263+ assert .Contains (t , recorder .msgs [0 ], "unexpected error" )
264+ }
265+
266+ // Invalid JSON must be reported, not called empty.
267+ func TestIsEmptyJSONReportsAParseFailure (t * testing.T ) {
268+ t .Parallel ()
269+
270+ recorder := & nonStoppingT {}
271+ empty := teststate .IsEmptyJSON (recorder , []byte ("{not json" ))
272+
273+ assert .True (t , recorder .failed , "the parse failure must be reported" )
274+ assert .False (t , empty , "invalid JSON is not the same as empty JSON" )
275+ }
0 commit comments