@@ -1229,8 +1229,79 @@ func TestEnforceFastestSchedule_NonUniformSecondsField(t *testing.T) {
12291229 maximumFastest := 5 * time .Second
12301230
12311231 jobDef := gocron .CronJob (schedule , true )
1232- capErr := enforceFastestSchedule (logger .Nop (), jobDef , maximumFastest )
1232+ capErr := enforceFastestSchedule (logger .Nop (), gocron . NewScheduler , jobDef , maximumFastest )
12331233 require .NotNil (t , capErr , "should reject schedule with 1s gaps" )
12341234 require .Equal (t , caperrors .LimitExceeded , capErr .Code ())
12351235 require .Contains (t , capErr .Error (), "maximum fastest cron schedule is 5s" )
12361236}
1237+
1238+ type mockScheduler struct {
1239+ gocron.Scheduler
1240+ shutdowns int
1241+ }
1242+
1243+ func (s * mockScheduler ) Shutdown () error {
1244+ s .shutdowns ++
1245+ return s .Scheduler .Shutdown ()
1246+ }
1247+
1248+ func mockFactory (mocks * []* mockScheduler ) schedulerFactory {
1249+ return func (options ... gocron.SchedulerOption ) (gocron.Scheduler , error ) {
1250+ sched , err := gocron .NewScheduler (options ... )
1251+ if err != nil {
1252+ return nil , err
1253+ }
1254+ mock := & mockScheduler {Scheduler : sched }
1255+ * mocks = append (* mocks , mock )
1256+ return mock , nil
1257+ }
1258+ }
1259+
1260+ // TestEnforceFastestSchedule_ShutsDownTempScheduler covers a regression where the
1261+ // temporary scheduler's Shutdown was deferred below the NewJob error check, so every
1262+ // workflow-supplied cron string that gocron failed to parse retained a scheduler
1263+ // goroutine for the life of the plugin. Each schedule below exercises a different
1264+ // return path through the check; all of them must dispose of the scheduler.
1265+ func TestEnforceFastestSchedule_ShutsDownTempScheduler (t * testing.T ) {
1266+ t .Parallel ()
1267+
1268+ cases := []struct {
1269+ name string
1270+ schedule string
1271+ shouldErr bool
1272+ }{
1273+ {name : "valid schedule" , schedule : everyMinute , shouldErr : false },
1274+ {name : "invalid timezone" , schedule : "TZ=moon * * * * *" , shouldErr : true },
1275+ {name : "empty schedule" , schedule : "" , shouldErr : true },
1276+ {name : "not a cron schedule" , schedule : "d d d d d" , shouldErr : true },
1277+ {name : "exceeds maximum fastest" , schedule : everySecond , shouldErr : true },
1278+ }
1279+
1280+ for _ , tt := range cases {
1281+ t .Run (tt .name , func (t * testing.T ) {
1282+ t .Parallel ()
1283+
1284+ var mocks []* mockScheduler
1285+ capErr := enforceFastestSchedule (logger .Nop (), mockFactory (& mocks ), gocron .CronJob (tt .schedule , true ), 5 * time .Second )
1286+ if tt .shouldErr {
1287+ require .NotNil (t , capErr , "schedule %q should have been rejected" , tt .schedule )
1288+ } else {
1289+ require .Nil (t , capErr )
1290+ }
1291+
1292+ require .Len (t , mocks , 1 , "expected exactly one temporary scheduler to be created" )
1293+ require .Equal (t , 1 , mocks [0 ].shutdowns , "temporary scheduler was not shut down on this path" )
1294+ })
1295+ }
1296+ }
1297+
1298+ func TestEnforceFastestSchedule_SchedulerConstructionFails (t * testing.T ) {
1299+ t .Parallel ()
1300+
1301+ failing := func (... gocron.SchedulerOption ) (gocron.Scheduler , error ) {
1302+ return nil , errors .New ("cannot construct" )
1303+ }
1304+ capErr := enforceFastestSchedule (logger .Nop (), failing , gocron .CronJob (everyMinute , true ), 5 * time .Second )
1305+ require .NotNil (t , capErr )
1306+ require .Equal (t , caperrors .Internal , capErr .Code ())
1307+ }
0 commit comments