@@ -99,7 +99,10 @@ describe("installFileLogSink", () => {
9999 const later = lineIndex ( active ) ;
100100 expect ( earlier ) . toBeLessThan ( later ) ;
101101 expect ( active ) . not . toContain ( `rotate-line-${ earlier } -` ) ;
102- expect ( Buffer . byteLength ( previous ) ) . toBeLessThanOrEqual ( 500 ) ;
102+ // The retained generation is the file that crossed the cap, so it can
103+ // pass the cap by the record that crossed it. The active file stays bounded.
104+ expect ( Buffer . byteLength ( previous ) ) . toBeGreaterThan ( 500 ) ;
105+ expect ( Buffer . byteLength ( previous ) ) . toBeLessThanOrEqual ( 500 + 200 ) ;
103106 expect ( Buffer . byteLength ( active ) ) . toBeLessThanOrEqual ( 500 ) ;
104107 expect ( previous + active ) . not . toContain ( "rotate-line-0-" ) ;
105108 expect ( active ) . toContain ( "rotate-line-39-" ) ;
@@ -108,6 +111,121 @@ describe("installFileLogSink", () => {
108111 }
109112 } ) ;
110113
114+ test ( "a second writer flushing one line keeps the other writer's rotated log" , ( ) => {
115+ const { dir, file } = tempLogFile ( ) ;
116+ try {
117+ installFileLogSink ( file , { maxBytes : 300 } ) ;
118+ const first = getLogger ( [ "logging" , "share-a" ] ) ;
119+ first . info ( "first-marker" ) ;
120+ first . info ( "first-marker-b" ) ;
121+ first . info ( "short-active" ) ;
122+ flushFileLogSinkSync ( ) ;
123+ const rotated = `${ file } .1` ;
124+ const marker = fs . readFileSync ( rotated , "utf8" ) ;
125+ const activeBefore = fs . readFileSync ( file , "utf8" ) ;
126+ expect ( marker ) . toContain ( "first-marker" ) ;
127+ expect ( activeBefore ) . toContain ( "short-active" ) ;
128+ expect ( Buffer . byteLength ( activeBefore ) ) . toBeLessThan ( 300 ) ;
129+
130+ installFileLogSink ( file , { maxBytes : 300 } ) ;
131+ getLogger ( [ "logging" , "share-b" ] ) . info ( "b-one" ) ;
132+ flushFileLogSinkSync ( ) ;
133+
134+ expect ( fs . readFileSync ( rotated , "utf8" ) ) . toBe ( marker ) ;
135+ const active = fs . readFileSync ( file , "utf8" ) ;
136+ expect ( active . startsWith ( activeBefore ) ) . toBe ( true ) ;
137+ expect ( active ) . toContain ( "b-one" ) ;
138+ } finally {
139+ fs . rmSync ( dir , { recursive : true , force : true } ) ;
140+ }
141+ } ) ;
142+
143+ test ( "crossing the cap once keeps the previous active file as .1" , ( ) => {
144+ const { dir, file } = tempLogFile ( ) ;
145+ try {
146+ installFileLogSink ( file , { maxBytes : 300 } ) ;
147+ const first = getLogger ( [ "logging" , "cross-a" ] ) ;
148+ first . info ( "first-marker" ) ;
149+ first . info ( "first-marker-b" ) ;
150+ first . info ( "short-active" ) ;
151+ flushFileLogSinkSync ( ) ;
152+ const rotated = `${ file } .1` ;
153+ const activeBefore = fs . readFileSync ( file , "utf8" ) ;
154+ expect ( activeBefore ) . toContain ( "short-active" ) ;
155+
156+ installFileLogSink ( file , { maxBytes : 300 } ) ;
157+ const second = getLogger ( [ "logging" , "cross-b" ] ) ;
158+ const wide = `b-wide-${ "x" . repeat ( 80 ) } ` ;
159+ second . info ( wide ) ;
160+ second . info ( `${ wide } -2` ) ;
161+ flushFileLogSinkSync ( ) ;
162+
163+ const kept = fs . readFileSync ( rotated , "utf8" ) ;
164+ expect ( kept ) . toContain ( "short-active" ) ;
165+ expect ( kept ) . not . toContain ( "b-wide-" ) ;
166+ const active = fs . readFileSync ( file , "utf8" ) ;
167+ expect ( active ) . toContain ( wide ) ;
168+ expect ( active ) . toContain ( `${ wide } -2` ) ;
169+ expect ( Buffer . byteLength ( activeBefore ) ) . toBeLessThan ( 300 ) ;
170+ } finally {
171+ fs . rmSync ( dir , { recursive : true , force : true } ) ;
172+ }
173+ } ) ;
174+
175+ test ( "a stale size does not rotate a short file another process just created" , ( ) => {
176+ const { dir, file } = tempLogFile ( ) ;
177+ try {
178+ installFileLogSink ( file , { maxBytes : 500 } ) ;
179+ const logger = getLogger ( [ "logging" , "stale" ] ) ;
180+ logger . info ( "x" . repeat ( 600 ) ) ;
181+ flushFileLogSinkSync ( ) ;
182+ expect ( fs . statSync ( file ) . size ) . toBeGreaterThan ( 500 ) ;
183+ expect ( fs . existsSync ( `${ file } .1` ) ) . toBe ( false ) ;
184+
185+ fs . rmSync ( file ) ;
186+ fs . writeFileSync ( file , "short-marker\n" ) ;
187+ logger . info ( "one-line" ) ;
188+ flushFileLogSinkSync ( ) ;
189+
190+ expect ( fs . existsSync ( `${ file } .1` ) ) . toBe ( false ) ;
191+ const active = fs . readFileSync ( file , "utf8" ) ;
192+ expect ( active ) . toContain ( "short-marker" ) ;
193+ expect ( active ) . toContain ( "one-line" ) ;
194+ } finally {
195+ fs . rmSync ( dir , { recursive : true , force : true } ) ;
196+ }
197+ } ) ;
198+
199+ test ( "renaming onto an existing generation still bounds the active file" , ( ) => {
200+ const { dir, file } = tempLogFile ( ) ;
201+ const renameSync = fs . renameSync . bind ( fs ) ;
202+ const rename = spyOn ( fs , "renameSync" ) . mockImplementation ( ( from , to ) => {
203+ if ( fs . existsSync ( to ) ) {
204+ throw Object . assign ( new Error ( "EPERM, rename" ) , { code : "EPERM" } ) ;
205+ }
206+ return renameSync ( from , to ) ;
207+ } ) ;
208+ try {
209+ installFileLogSink ( file , { maxBytes : 500 } ) ;
210+ const logger = getLogger ( [ "logging" , "eperm" ] ) ;
211+ for ( let i = 0 ; i < 40 ; i ++ ) {
212+ logger . info ( `eperm-line-${ i } -xxxxxxxxxxxxxxxxxxxx` ) ;
213+ }
214+ flushFileLogSinkSync ( ) ;
215+ expect ( fs . statSync ( `${ file } .1` ) . isFile ( ) ) . toBe ( true ) ;
216+ expect ( fs . statSync ( file ) . size ) . toBeLessThanOrEqual ( 500 + 200 ) ;
217+ for ( let i = 0 ; i < 10 ; i ++ ) {
218+ logger . info ( `eperm-more-${ i } -xxxxxxxxxxxxxxxxxxxx` ) ;
219+ }
220+ flushFileLogSinkSync ( ) ;
221+ expect ( fs . statSync ( file ) . size ) . toBeLessThanOrEqual ( 500 + 200 ) ;
222+ expect ( fs . readFileSync ( file , "utf8" ) . length ) . toBeGreaterThan ( 0 ) ;
223+ } finally {
224+ rename . mockRestore ( ) ;
225+ fs . rmSync ( dir , { recursive : true , force : true } ) ;
226+ }
227+ } ) ;
228+
111229 test ( "a failed rotation keeps already-written records in the active file" , ( ) => {
112230 const { dir, file } = tempLogFile ( ) ;
113231 try {
0 commit comments