# cronex

# 简介

用于类 corn 任务调度的 Python 模块,提供一个接口用于评估和触发 corn 表达式。

  • 类 corn 任务:Cron 是一种时间表驱动的任务调度程序,用于在特定的时间和日期执行任务。
  • Corn 表达式:由 5~6 个字段组成,分别表示分钟、小时、日、月、星期和可选的年份

其表达式如下:

1
2
3
4
5
6
7
8
9
10
* * * * *  command to be executed
- - - - -
| | | | |
| | | | ----- 星期 (0 - 7) (星期日 = 07)
| | | ------- 月份 (1 - 12)
| | --------- 月中的日期 (1 - 31)
| ----------- 小时 (0 - 23)
------------- 分钟 (0 - 59)

0 1 * * * /path/to/command #表示每天凌晨1点执行某个命令

# cronex 功能

  • 支持 Java Quartz 的特殊字符,如 "L"、"W" 和 "#"。
  • 允许设定任意间隔的触发器,例如每 9 小时,每 11 分钟等。
  • 示例代码:

1
2
3
job = CronExpression("0 0 * * 1-5/2 find /var/log -delete")
print(job.check_trigger((2010, 11, 17, 0, 0))) # True
print(job.check_trigger((2012, 12, 21, 0, 0))) # False

这里的 CronExpression ("0 0 * * 1-5/2 find /var/log -delete") 创建了一个 CronExpression 对象,其 cron 表达式为 0 0 * * 1-5/2

  • 0 0 :每天的 00:00
  • * * : 每个月的每一天
  • 1-5/2 : 星期一到星期五中每隔一天
  • find /var/log -delete :命令部分

当给这个 job 传递参数 job.check_trigger ((2010, 11, 17, 0, 0)) 时:

  • 日期时间:2010 年 11 月 17 日 00:00
  • 星期三符合周一~周五间隔一天
  • 符合 corn 表达式条件,输出 True

# tests.py 代码处理

# 更正说明

由于本人的 python 版本是 3.12,直接运行 tests.py 会报错

1
2
3
4
5
6
Error
Traceback (most recent call last):
File "D:\desktop\Code_Compiling\LLM的test_oracle生成\20_cronex-master\cronex-master\cronex\tests.py", line 397, in test_catches_bad_L
self.failUnlessRaises(ValueError,
^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'test_testedmodule' object has no attribute 'failUnlessRaises'

原因是 unittest.TestCase 类中没有名为 failUnlessRaises 的方法

这是由于该代码使用了旧版本 unittest 中的方法名称,现在应该使用 assertRaises 方法。

更正后的 tests.py 代码如下:

# debug 后的 tests

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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import calendar
import datetime
import os
import site
import sys
import time
import unittest

MINUTE = 60
HOUR = 60 * MINUTE
DAY = 24 * HOUR

# Python 3 compatibility
if isinstance(map, type):
xrange = range

# Add the parent directory relative to the test case because we assume the
# test will be in a subdirectory relative to the main code base.
sys.path.insert(0, os.path.join(
os.path.dirname(os.path.realpath(__file__)), os.pardir))

import cronex


class test_testedmodule(unittest.TestCase):
def test_substitution(self):
"""
Verify that the special substitutions are replaced by the correct,
expanded cron expressions.
"""
testcases = [("@yearly", "0 0 1 1 *"),
("@annually", "0 0 1 1 *"),
("@monthly", "0 0 1 * *"),
("@weekly", "0 0 * * 0"),
("@daily", "0 0 * * *"),
("@midnight", "0 0 * * *"),
("@hourly", "0 * * * *")]
for a, b in testcases:
obj = cronex.CronExpression(a)
self.assertTrue(b in repr(obj))

def test_compute_numtab(self):
"""
Verify that calling compute_numtab after modifying the string-version
of an expression results in the static trigger-value cache being
updated.
"""
testex1 = cronex.CronExpression("*/7 5-10 5 * *")
testex2 = cronex.CronExpression("*/5 23-2 5 8 *")
self.assertNotEqual(testex1.string_tab, testex2.string_tab)
self.assertNotEqual(testex1.numerical_tab, testex2.numerical_tab)
testex1.string_tab = testex2.string_tab
testex1.compute_numtab()
self.assertEqual(testex1.string_tab, testex2.string_tab)
self.assertEqual(testex1.numerical_tab, testex2.numerical_tab)

def test_periodics_dom(self):
"""
Verify that arbitrary-period repeaters in the day-of-the-month field
work as expected.
"""
now = int(time.time())
then = time.gmtime(now - 491 * DAY)
now_tuple = time.gmtime(now)
testex = cronex.CronExpression("* * %491 * *")
self.assertFalse(testex.check_trigger(now_tuple[:5]))
testex.epoch = tuple(list(then[:5]) + [0])
self.assertTrue(testex.check_trigger(now_tuple[:5]))
self.assertTrue(testex.check_trigger(then[:5]))

def test_periodics_hours(self):
"""
Verify that arbitrary-period repeaters in the hours field work as
expected.
"""
now = int(time.time())
then = time.gmtime(now - 9001 * HOUR)
now_tuple = time.gmtime(now)
testex = cronex.CronExpression("* %9001 * * *")
self.assertFalse(testex.check_trigger(now_tuple[:5]))
testex.epoch = tuple(list(then[:5]) + [0])
self.assertTrue(testex.check_trigger(now_tuple[:5]))
self.assertTrue(testex.check_trigger(then[:5]))

def test_periodics_minutes(self):
"""
Verify that arbitrary-period repeaters in the minutes field work as
expected.
"""
now = int(time.time())
then = time.gmtime(now - 814075 * MINUTE)
now_tuple = time.gmtime(now)
testex = cronex.CronExpression("%814075 * * * *")
self.assertFalse(testex.check_trigger(now_tuple[:5]))
testex.epoch = tuple(list(then[:5]) + [0])
self.assertTrue(testex.check_trigger(now_tuple[:5]))
self.assertTrue(testex.check_trigger(then[:5]))

def test_periodics_month(self):
"""
Verify that arbitrary-period repeaters in the month field work as
expected. More specifically, it verifies that the repeaters are
triggered every "N" calendar months regardless of the day for triggers
set to go offer every 1 month, ever 2 months, etc... through 18 months
from 1970 through 1980.
"""
for period in range(2, 18):
calendar_months = 0
description = "* * * %%%i *" % period
cron_expression = cronex.CronExpression(description)

for year in range(1970, 1980):
for month in range(1, 13):
days_in_month = calendar.monthrange(year, month)[-1]
should_trigger = not (calendar_months % period)

for day in range(1, days_in_month + 1):
time_tuple = (year, month, day, 0, 0)
triggered = cron_expression.check_trigger(time_tuple)
self.assertEqual(should_trigger, triggered,
"%s: expected trigger to be %r for %r" %
(description, should_trigger, time_tuple)
)

calendar_months += 1

def test_parse_atom(self):
"""
Verify that parsing atoms returns sets containing all of the expected
values.
"""
input_expect = [
(('5-10', (1, 20)), set([5, 6, 7, 8, 9, 10])),
(('10-5', (1, 12)), set([10, 11, 12, 1, 2, 3, 4, 5])),
(('5-10/2', (1, 20)), set([5, 7, 9])),
(('10-5/2', (1, 12)), set([10, 12, 2, 4])),
(('11-5/2', (1, 12)), set([11, 1, 3, 5])),
(('10-5/3', (1, 11)), set([10, 2, 5])),
(('11-5/3', (1, 14)), set([11, 14, 3])),
(('*', (1, 100)), set(xrange(1, 101))),
(('*/5', (1, 100)), set(xrange(1, 101, 5))),
(('666', (1, 1000)), set([666])),
(('21-1', (0, 23)), set([21, 22, 23, 0, 1])),
(('5/15', (0, 60)), set([5, 20, 35, 50])),
(('1/4', (0, 23)), set([1, 5, 9, 13, 17, 21])),
]

for give_the_function, want_from_function in input_expect:
self.assertEqual(want_from_function,
cronex.parse_atom(*give_the_function))

def test_str_and_repr(self):
"""
Verify that the __repr__ and __str__ return values can be passed to
eval to generate an identical CronExpression.
"""
CronExpression = cronex.CronExpression
testex1 = cronex.CronExpression("*/15 4 1-7 * * TEST___TEST")
testex2 = eval(repr(testex1))
self.assertEqual(testex1.string_tab, testex2.string_tab)
self.assertEqual(testex1.numerical_tab, testex2.numerical_tab)
self.assertEqual(testex1.comment, testex2.comment)
self.assertEqual(repr(testex1), str(testex1))

def test_dom_substitution(self):
"""
Verify that shortened month names are correctly translated numeric
indexes.
"""
testex1 = cronex.CronExpression(
"* * * jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec *")
testex2 = cronex.CronExpression("* * * 1,2,3,4,5,6,7,8,9,10,11,12 *")
self.assertEqual(repr(testex1), repr(testex2))

def test_dow_substitution(self):
"""
Verify that shortened days-of-the-week are correctly translated numeric
indexes.
"""
testex1 = cronex.CronExpression("* * * * sun,mon,tue,wed,thu,fri,sat")
testex2 = cronex.CronExpression("* * * * 0,1,2,3,4,5,6")
self.assertEqual(repr(testex1), repr(testex2))

def test_dom_either_or_dow(self):
"""
Verify that combining day of-the-month field and day-of-the-week field
triggers on either condition matching the current day and that using a
wild-card day of the month with a day of the week entry doesn't trigger
every single day.
"""
# Trigger should fire every Monday or on the 5th day of the month.
testex = cronex.CronExpression("0 0 5 * mon")
for e in xrange(1, 30):
if e in (1, 5, 8, 15, 22, 29):
self.assertTrue(testex.check_trigger((2010, 11, e, 0, 0)))
else:
self.assertFalse(testex.check_trigger((2010, 11, e, 0, 0)))

# Trigger should fire every Wednesday at midnight.
testex = cronex.CronExpression("0 0 * * wed")
for d in xrange(1, 32):
# In July of 2014, every Wednesday just happens to be on a day of
# the month that's a multiple of 7.
if not(d % 7):
self.assertTrue(testex.check_trigger((2010, 7, d, 0, 0)))
else:
self.assertFalse(testex.check_trigger((2010, 7, d, 0, 0)))

def test_L_in_dow(self):
"""
Verify that having L in the day-of-the-week field with a number always
triggers on last occurrence of the corresponding day of the week in any
given month.
"""
testex = cronex.CronExpression("0 0 * * 6L")
# The numbers are the dates of the last Saturday in each month of the
# year 2010.
tv = [30, 27, 27, 24, 29, 26, 31, 28, 25, 30, 27, 25]
for v in xrange(0, 12):
self.assertTrue((testex.check_trigger((2010, v+1, tv[v], 0, 0))))

def test_L_in_dom(self):
"""
Verify that having L in the day-of-the-month field always triggers on
the last day of the month.
"""
testex = cronex.CronExpression("0 0 L * *")
for y in xrange(2000, 2009):
for v in xrange(1, 13):
# Checks every day from January 1st 2000 through December 31st
# 2008 an verifies that the trigger only activates on the last
# day of the month. The year range was chosen to ensure at leas
# one leap year would be included in the test.
lastdom = calendar.monthrange(y, v)[-1]
for d in xrange(1, lastdom + 1):
if d < lastdom:
self.assertFalse(testex.check_trigger((y, v, d, 0, 0)))
else:
self.assertTrue(testex.check_trigger((y, v, d, 0, 0)))

def test_calendar_change_vs_hour_change(self):
"""
Verify that a periodic trigger for the day of the month is based on
calendar days, not 24-hour days.
"""
# epoch and local differ by < 48 hours but it should be reported based
# on calendar days, not 24 hour days
epoch = (2010, 11, 16, 23, 59)
local_time = (2010, 11, 18, 0, 0)
testex = cronex.CronExpression("0 0 %2 * *", epoch, -6)
self.assertTrue(testex.check_trigger(local_time, -6))

def test_asterisk_is_loney(self):
"""
Verify that asterisk cannot be combined with other atoms.
"""
self.assertRaises(ValueError,
cronex.CronExpression, "* *,1-9 * * *")

def test_dow_occurence(self):
"""
Verify that using "#" to find the Nth occurrence of a given day of the
week works correctly.
"""
for dow in xrange(0, 7):
for occurence in (1, 6):
day = (7 * (occurence - 1)) + dow + 1
expression = "0 0 * * %i#%i" % (dow, occurence)
if occurence > 5:
# There can never be more than 5 occurrences of a given day
# of the week in one month.
self.assertRaises(ValueError, cronex.CronExpression,
expression)
elif day < 32:
testex = cronex.CronExpression(expression)
self.assertTrue(testex.check_trigger(
(2011, 5, day, 0, 0)))
if day > 8:
self.assertFalse(testex.check_trigger(
(2011, 5, max(day - 7, 1), 0, 0)))
elif day < 25:
self.assertFalse(testex.check_trigger(
(2011, 5, max(day + 7, 1), 0, 0)))

def test_strict_range_bounds(self):
"""
Verify that having numbers outside of the reasonable ranges for each
field raises an exception. Tests both upper and lower bounds.
"""
self.assertRaises(ValueError,
cronex.CronExpression, "1000 * * * *")
self.assertRaises(ValueError,
cronex.CronExpression, "* 1000 * * *")
self.assertRaises(ValueError,
cronex.CronExpression, "* * 1000 * *")
self.assertRaises(ValueError,
cronex.CronExpression, "* * * 1000 *")
self.assertRaises(ValueError,
cronex.CronExpression, "* * * * 1000")
self.assertRaises(ValueError,
cronex.CronExpression, "-1 * * * *")
self.assertRaises(ValueError,
cronex.CronExpression, "* -1 * * *")
self.assertRaises(ValueError,
cronex.CronExpression, "* * 0 * *")
self.assertRaises(ValueError,
cronex.CronExpression, "* * * 0 *")
self.assertRaises(ValueError,
cronex.CronExpression, "* * * * -1")

def test_catches_bad_modulus(self):
"""
Verify that having a trigger with an invalid repetition period raises
an exception.
"""
badstuff = [
"* * * * %-1",
"%1 * * * *",
"* %1 * * *",
"* * %1 * *",
"* * * %1 *",
"* * * * %1",
"%0 * * * *",
"* %0 * * *",
"* * %0 * *",
"* * * %0 *",
"* * * * %0"
]
for case in badstuff:
self.assertRaises(ValueError,
cronex.CronExpression, case)

def test_catches_bad_W(self):
"""
Verify that having a syntactically invalid "W" raises an exception.
"""
badstuff = [
"5W * * * *",
"* 5W * * *",
"* * 99W * *",
"* * 0W * *",
"* * W0 * *",
"* * * 5W *",
"* * * * 5W",
]
for case in badstuff:
self.assertRaises(ValueError,
cronex.CronExpression, case)

def test_catches_bad_L(self):
"""
Verify that having a syntactically invalid "L" raises an exception.
"""
badstuff = [
"L * * * *",
"* L * * *",
"* * 99L * *",
"* * 0L * *",
"* * * L *",
"* * * * L",
"* * * * 9L",
"* * * * -9L"]
for case in badstuff:
self.assertRaises(ValueError,
cronex.CronExpression, case)

def test_catches_bad_Pound(self):
"""
Verify that having a syntactically invalid "#" raises an exception.
"""
badstuff = [
"# * * * *",
"* # * * *",
"* * # * *",
"* * * # *",
"* * * * 9#9L"]
for case in badstuff:
self.assertRaises(ValueError,
cronex.CronExpression, case)

def test_fail_on_not_enough_fields(self):
"""
Verify that an exception is thrown when the cron expression has too few
fields.
"""
badstuff = ["*", "* *", "* * *", "* * * *"]
for case in badstuff:
self.assertRaises(ValueError, cronex.CronExpression, case)


if __name__ == "__main__":
unittest.main()

现在去除所有 assert 语句,并将去除 assert 后的 tests 代码补全逻辑后,得到

# 去除 assert 及相关提示后的 tests_code

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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import calendar
import datetime
import os
import site
import sys
import time
import unittest

MINUTE = 60
HOUR = 60 * MINUTE
DAY = 24 * HOUR

# Python 3 compatibility
if isinstance(map, type):
xrange = range

# Add the parent directory relative to the test case because we assume the
# test will be in a subdirectory relative to the main code base.
sys.path.insert(0, os.path.join(
os.path.dirname(os.path.realpath(__file__)), os.pardir))

import cronex


class test_testedmodule(unittest.TestCase):
def test_substitution(self):
"""
Verify that the special substitutions are replaced by the correct,
expanded cron expressions.
"""
testcases = [("@yearly", "0 0 1 1 *"),
("@annually", "0 0 1 1 *"),
("@monthly", "0 0 1 * *"),
("@weekly", "0 0 * * 0"),
("@daily", "0 0 * * *"),
("@midnight", "0 0 * * *"),
("@hourly", "0 * * * *")]


def test_compute_numtab(self):
testex1 = cronex.CronExpression("*/7 5-10 5 * *")
testex2 = cronex.CronExpression("*/5 23-2 5 8 *")
testex1.string_tab = testex2.string_tab
testex1.compute_numtab()

def test_periodics_dom(self):
now = int(time.time())
then = time.gmtime(now - 491 * DAY)
now_tuple = time.gmtime(now)
testex = cronex.CronExpression("* * %491 * *")
testex.epoch = tuple(list(then[:5]) + [0])

def test_periodics_hours(self):
now = int(time.time())
then = time.gmtime(now - 9001 * HOUR)
now_tuple = time.gmtime(now)
testex = cronex.CronExpression("* %9001 * * *")
testex.epoch = tuple(list(then[:5]) + [0])

def test_periodics_minutes(self):
now = int(time.time())
then = time.gmtime(now - 814075 * MINUTE)
now_tuple = time.gmtime(now)
testex = cronex.CronExpression("%814075 * * * *")
testex.epoch = tuple(list(then[:5]) + [0])

def test_periodics_month(self):
for period in range(2, 18):
calendar_months = 0
description = "* * * %%%i *" % period
cron_expression = cronex.CronExpression(description)

for year in range(1970, 1980):
for month in range(1, 13):
days_in_month = calendar.monthrange(year, month)[-1]
should_trigger = not (calendar_months % period)

for day in range(1, days_in_month + 1):
time_tuple = (year, month, day, 0, 0)
triggered = cron_expression.check_trigger(time_tuple)

calendar_months += 1

def test_parse_atom(self):
input_expect = [
(('5-10', (1, 20)), set([5, 6, 7, 8, 9, 10])),
(('10-5', (1, 12)), set([10, 11, 12, 1, 2, 3, 4, 5])),
(('5-10/2', (1, 20)), set([5, 7, 9])),
(('10-5/2', (1, 12)), set([10, 12, 2, 4])),
(('11-5/2', (1, 12)), set([11, 1, 3, 5])),
(('10-5/3', (1, 11)), set([10, 2, 5])),
(('11-5/3', (1, 14)), set([11, 14, 3])),
(('*', (1, 100)), set(xrange(1, 101))),
(('*/5', (1, 100)), set(xrange(1, 101, 5))),
(('666', (1, 1000)), set([666])),
(('21-1', (0, 23)), set([21, 22, 23, 0, 1])),
(('5/15', (0, 60)), set([5, 20, 35, 50])),
(('1/4', (0, 23)), set([1, 5, 9, 13, 17, 21])),
]

for give_the_function, want_from_function in input_expect:
cronex.parse_atom(*give_the_function)

def test_str_and_repr(self):
CronExpression = cronex.CronExpression
testex1 = cronex.CronExpression("*/15 4 1-7 * * TEST___TEST")
testex2 = eval(repr(testex1))

def test_dom_substitution(self):
testex1 = cronex.CronExpression(
"* * * jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec *")
testex2 = cronex.CronExpression("* * * 1,2,3,4,5,6,7,8,9,10,11,12 *")

def test_dow_substitution(self):
testex1 = cronex.CronExpression("* * * * sun,mon,tue,wed,thu,fri,sat")
testex2 = cronex.CronExpression("* * * * 0,1,2,3,4,5,6")

def test_dom_either_or_dow(self):
# Trigger should fire every Monday or on the 5th day of the month.
testex = cronex.CronExpression("0 0 5 * mon")
for e in xrange(1, 30):
pass

# Trigger should fire every Wednesday at midnight.
testex = cronex.CronExpression("0 0 * * wed")
for d in xrange(1, 32):
pass

def test_L_in_dow(self):
testex = cronex.CronExpression("0 0 * * 6L")
# The numbers are the dates of the last Saturday in each month of the
# year 2010.
tv = [30, 27, 27, 24, 29, 26, 31, 28, 25, 30, 27, 25]

def test_L_in_dom(self):
testex = cronex.CronExpression("0 0 L * *")
for y in xrange(2000, 2009):
for v in xrange(1, 13):
# Checks every day from January 1st 2000 through December 31st
# 2008 an verifies that the trigger only activates on the last
# day of the month. The year range was chosen to ensure at leas
# one leap year would be included in the test.
lastdom = calendar.monthrange(y, v)[-1]
for d in xrange(1, lastdom + 1):
pass

def test_calendar_change_vs_hour_change(self):
# epoch and local differ by < 48 hours but it should be reported based
# on calendar days, not 24 hour days
epoch = (2010, 11, 16, 23, 59)
local_time = (2010, 11, 18, 0, 0)
testex = cronex.CronExpression("0 0 %2 * *", epoch, -6)

def test_asterisk_is_loney(self):
pass

def test_dow_occurence(self):
for dow in xrange(0, 7):
for occurence in (1, 6):
day = (7 * (occurence - 1)) + dow + 1
expression = "0 0 * * %i#%i" % (dow, occurence)
if occurence > 5:
pass
elif day < 32:
testex = cronex.CronExpression(expression)

if day > 8:
pass
elif day < 25:
pass

def test_strict_range_bounds(self):
pass

def test_catches_bad_modulus(self):

badstuff = [
"* * * * %-1",
"%1 * * * *",
"* %1 * * *",
"* * %1 * *",
"* * * %1 *",
"* * * * %1",
"%0 * * * *",
"* %0 * * *",
"* * %0 * *",
"* * * %0 *",
"* * * * %0"
]
for case in badstuff:
pass

def test_catches_bad_W(self):
badstuff = [
"5W * * * *",
"* 5W * * *",
"* * 99W * *",
"* * 0W * *",
"* * W0 * *",
"* * * 5W *",
"* * * * 5W",
]
for case in badstuff:
pass

def test_catches_bad_L(self):
badstuff = [
"L * * * *",
"* L * * *",
"* * 99L * *",
"* * 0L * *",
"* * * L *",
"* * * * L",
"* * * * 9L",
"* * * * -9L"]
for case in badstuff:
pass

def test_catches_bad_Pound(self):
badstuff = [
"# * * * *",
"* # * * *",
"* * # * *",
"* * * # *",
"* * * * 9#9L"]
for case in badstuff:
pass

def test_fail_on_not_enough_fields(self):
badstuff = ["*", "* *", "* * *", "* * * *"]
for case in badstuff:
pass


if __name__ == "__main__":
unittest.main()

# gpt3.5 生成补全后的 tests

将 tests_code 投喂给 gpt3.5,并且附上 prompt 该 tests case 缺少 assert 语句,请你自动补全

补全后的代码为:

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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import calendar
import datetime
import os
import site
import sys
import time
import unittest

MINUTE = 60
HOUR = 60 * MINUTE
DAY = 24 * HOUR

# Python 3 compatibility
if isinstance(map, type):
xrange = range

# Add the parent directory relative to the test case because we assume the
# test will be in a subdirectory relative to the main code base.
sys.path.insert(0, os.path.join(
os.path.dirname(os.path.realpath(__file__)), os.pardir))

import cronex


class test_testedmodule(unittest.TestCase):
def test_substitution(self):
"""
Verify that the special substitutions are replaced by the correct,
expanded cron expressions.
"""
testcases = [("@yearly", "0 0 1 1 *"),
("@annually", "0 0 1 1 *"),
("@monthly", "0 0 1 * *"),
("@weekly", "0 0 * * 0"),
("@daily", "0 0 * * *"),
("@midnight", "0 0 * * *"),
("@hourly", "0 * * * *")]

for special, expected in testcases:
cron_expression = cronex.CronExpression(special)
self.assertEqual(str(cron_expression), expected)

def test_compute_numtab(self):
testex1 = cronex.CronExpression("*/7 5-10 5 * *")
testex2 = cronex.CronExpression("*/5 23-2 5 8 *")
testex1.string_tab = testex2.string_tab
testex1.compute_numtab()
self.assertEqual(testex1.string_tab, testex2.string_tab)

def test_periodics_dom(self):
now = int(time.time())
then = time.gmtime(now - 491 * DAY)
now_tuple = time.gmtime(now)
testex = cronex.CronExpression("* * %491 * *")
testex.epoch = tuple(list(then[:5]) + [0])
self.assertTrue(testex.check_trigger(now_tuple))

def test_periodics_hours(self):
now = int(time.time())
then = time.gmtime(now - 9001 * HOUR)
now_tuple = time.gmtime(now)
testex = cronex.CronExpression("* %9001 * * *")
testex.epoch = tuple(list(then[:5]) + [0])
self.assertTrue(testex.check_trigger(now_tuple))

def test_periodics_minutes(self):
now = int(time.time())
then = time.gmtime(now - 814075 * MINUTE)
now_tuple = time.gmtime(now)
testex = cronex.CronExpression("%814075 * * * *")
testex.epoch = tuple(list(then[:5]) + [0])
self.assertTrue(testex.check_trigger(now_tuple))

def test_periodics_month(self):
for period in range(2, 18):
calendar_months = 0
description = "* * * %%%i *" % period
cron_expression = cronex.CronExpression(description)

for year in range(1970, 1980):
for month in range(1, 13):
days_in_month = calendar.monthrange(year, month)[-1]
should_trigger = not (calendar_months % period)

for day in range(1, days_in_month + 1):
time_tuple = (year, month, day, 0, 0)
triggered = cron_expression.check_trigger(time_tuple)
self.assertEqual(triggered, should_trigger)

calendar_months += 1

def test_parse_atom(self):
input_expect = [
(('5-10', (1, 20)), set([5, 6, 7, 8, 9, 10])),
(('10-5', (1, 12)), set([10, 11, 12, 1, 2, 3, 4, 5])),
(('5-10/2', (1, 20)), set([5, 7, 9])),
(('10-5/2', (1, 12)), set([10, 12, 2, 4])),
(('11-5/2', (1, 12)), set([11, 1, 3, 5])),
(('10-5/3', (1, 11)), set([10, 2, 5])),
(('11-5/3', (1, 14)), set([11, 14, 3])),
(('*', (1, 100)), set(xrange(1, 101))),
(('*/5', (1, 100)), set(xrange(1, 101, 5))),
(('666', (1, 1000)), set([666])),
(('21-1', (0, 23)), set([21, 22, 23, 0, 1])),
(('5/15', (0, 60)), set([5, 20, 35, 50])),
(('1/4', (0, 23)), set([1, 5, 9, 13, 17, 21])),
]

for give_the_function, want_from_function in input_expect:
self.assertEqual(cronex.parse_atom(*give_the_function), want_from_function)

def test_str_and_repr(self):
CronExpression = cronex.CronExpression
testex1 = cronex.CronExpression("*/15 4 1-7 * * TEST___TEST")
testex2 = eval(repr(testex1))
self.assertEqual(testex1, testex2)

def test_dom_substitution(self):
testex1 = cronex.CronExpression(
"* * * jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec *")
testex2 = cronex.CronExpression("* * * 1,2,3,4,5,6,7,8,9,10,11,12 *")
self.assertEqual(testex1, testex2)

def test_dow_substitution(self):
testex1 = cronex.CronExpression("* * * * sun,mon,tue,wed,thu,fri,sat")
testex2 = cronex.CronExpression("* * * * 0,1,2,3,4,5,6")
self.assertEqual(testex1, testex2)

def test_dom_either_or_dow(self):
# Trigger should fire every Monday or on the 5th day of the month.
testex = cronex.CronExpression("0 0 5 * mon")
for e in xrange(1, 30):
now = (2024, 7, e, 0, 0)
self.assertTrue(testex.check_trigger(now) == (now[2] == 5 or time.strftime("%a", time.gmtime(time.mktime(now))) == "Mon"))

# Trigger should fire every Wednesday at midnight.
testex = cronex.CronExpression("0 0 * * wed")
for d in xrange(1, 32):
now = (2024, 7, d, 0, 0)
self.assertTrue(testex.check_trigger(now) == (time.strftime("%a", time.gmtime(time.mktime(now))) == "Wed"))

def test_L_in_dow(self):
testex = cronex.CronExpression("0 0 * * 6L")
# The numbers are the dates of the last Saturday in each month of the
# year 2010.
tv = [30, 27, 27, 24, 29, 26, 31, 28, 25, 30, 27, 25]
for i, day in enumerate(tv):
now = (2010, i + 1, day, 0, 0)
self.assertTrue(testex.check_trigger(now))

def test_L_in_dom(self):
testex = cronex.CronExpression("0 0 L * *")
for y in xrange(2000, 2009):
for v in xrange(1, 13):
# Checks every day from January 1st 2000 through December 31st
# 2008 and verifies that the trigger only activates on the last
# day of the month. The year range was chosen to ensure at least
# one leap year would be included in the test.
lastdom = calendar.monthrange(y, v)[-1]
for d in xrange(1, lastdom + 1):
now = (y, v, d, 0, 0)
self.assertTrue(testex.check_trigger(now) == (d == lastdom))

def test_calendar_change_vs_hour_change(self):
# epoch and local differ by < 48 hours but it should be reported based
# on calendar days, not 24 hour days
epoch = (2010, 11, 16, 23, 59)
local_time = (2010, 11, 18, 0, 0)
testex = cronex.CronExpression("0 0 %2 * *", epoch, -6)
self.assertTrue(testex.check_trigger(local_time))

def test_asterisk_is_loney(self):
testex = cronex.CronExpression("* * * * *")
now = time.gmtime()
self.assertTrue(testex.check_trigger(now))

def test_dow_occurence(self):
for dow in xrange(0, 7):
for occurence in (1, 6):
day = (7 * (occurence - 1)) + dow + 1
expression = "0 0 * * %i#%i" % (dow, occurence)
if occurence > 5:
testex = cronex.CronExpression(expression)
now = (2024, 7, day, 0, 0)
self.assertFalse(testex.check_trigger(now))
elif day < 32:
testex = cronex.CronExpression(expression)
now = (2024, 7, day, 0, 0)
self.assertTrue(testex.check_trigger(now) == (day <= calendar.monthrange(2024, 7)[-1]))

def test_strict_range_bounds(self):
with self.assertRaises(ValueError):
cronex.CronExpression("*/61 * * * *")

def test_catches_bad_modulus(self):
badstuff = [
"* * * * %-1",
"%1 * * * *",
"* %1 * * *",
"* * %1 * *",
"* * * %1 *",
"* * * * %1",
"%0 * * * *",
"* %0 * * *",
"* * %0 * *",
"* * * %0 *",
"* * * * %0"
]
for case in badstuff:
with self.assertRaises(ValueError):
cronex.CronExpression(case)

def test_catches_bad_W(self):
badstuff = [
"5W * * * *",
"* 5W * * *",
"* * 99W * *",
"* * 0W * *",
"* * W0 * *",
"* * * 5W *",
"* * * * 5W",
]
for case in badstuff:
with self.assertRaises(ValueError):
cronex.CronExpression(case)

def test_catches_bad_L(self):
badstuff = [
"L * * * *",
"* L * * *",
"* * 99L * *",
"* * 0L * *",
"* * * L *",
"* * * * L",
"* * * * 9L",
"* * * * -9L"]
for case in badstuff:
with self.assertRaises(ValueError):
cronex.CronExpression(case)

def test_catches_bad_Pound(self):
badstuff = [
"# * * * *",
"* # * * *",
"* * # * *",
"* * * # *",
"* * * * 9#9L"]
for case in badstuff:
with self.assertRaises(ValueError):
cronex.CronExpression(case)

def test_fail_on_not_enough_fields(self):
badstuff = ["*", "* *", "* * *", "* * * *"]
for case in badstuff:
with self.assertRaises(ValueError):
cronex.CronExpression(case)


if __name__ == "__main__":
unittest.main()

# gpt 自动填充后 assert 的优缺点分析

# 填充优秀部分:

# 1.test_substitution()

tests 原代码块函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def test_substitution(self):
"""
Verify that the special substitutions are replaced by the correct,
expanded cron expressions.
"""
testcases = [("@yearly", "0 0 1 1 *"),
("@annually", "0 0 1 1 *"),
("@monthly", "0 0 1 * *"),
("@weekly", "0 0 * * 0"),
("@daily", "0 0 * * *"),
("@midnight", "0 0 * * *"),
("@hourly", "0 * * * *")]
for a, b in testcases:
obj = cronex.CronExpression(a)
self.assertTrue(b in repr(obj))

gpt3.5 自动填充的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def test_substitution(self):
"""
Verify that the special substitutions are replaced by the correct,
expanded cron expressions.
"""
testcases = [("@yearly", "0 0 1 1 *"),
("@annually", "0 0 1 1 *"),
("@monthly", "0 0 1 * *"),
("@weekly", "0 0 * * 0"),
("@daily", "0 0 * * *"),
("@midnight", "0 0 * * *"),
("@hourly", "0 * * * *")]

for special, expected in testcases:
cron_expression = cronex.CronExpression(special)
self.assertEqual(str(cron_expression), expected)

  • 原本的 test 代码中使用 assertTrue 来验证特殊替换后的字符串是否在 objrepr 中存在,通过检查 repr(obj) 的输出来间接验证该特殊替换是否正确
  • 而 gpt 填充的代码直接使用 assertEqual 来验证 cron_expression 对象的 str() 方法是否生成了预期的替换后的 cron 表达式。使用 asserEqual 来判断两个值是否相等
  • gpt 自动填充的代码更优秀 —— 其使用 special 和 expected 来作为变量名,更直观和可读。并且它不依赖于间接验证 repr 的输出是否包含特定字符串,而是直接检查 str() 方法生成的输出是否符合预期。通过直接比较字符串可以更清晰地表达测试的意图和预期结果,使得测试代码更易于理解和维护。
  • 综上所述,在 test_substitution 函数上,gpt 生成的代码更加优秀。

# 填充后不足 / 有缺陷部分

  1. test_compute_numtab()

tests 原代码:

1
2
3
4
5
6
7
8
9
def test_compute_numtab(self):
testex1 = cronex.CronExpression("*/7 5-10 5 * *")
testex2 = cronex.CronExpression("*/5 23-2 5 8 *")
self.assertNotEqual(testex1.string_tab, testex2.string_tab)
self.assertNotEqual(testex1.numerical_tab, testex2.numerical_tab)
testex1.string_tab = testex2.string_tab
testex1.compute_numtab()
self.assertEqual(testex1.string_tab, testex2.string_tab)
self.assertEqual(testex1.numerical_tab, testex2.numerical_tab)

gpt3.5 自动填充代码

1
2
3
4
5
6
def test_compute_numtab(self):
testex1 = cronex.CronExpression("*/7 5-10 5 * *")
testex2 = cronex.CronExpression("*/5 23-2 5 8 *")
testex1.string_tab = testex2.string_tab
testex1.compute_numtab()
self.assertEqual(testex1.string_tab, testex2.string_tab)

  • 原代码验证在修改表达式的字符串版本后调用 compute_numtab 是否会更新静态触发器值缓存。
  • 原代码在最开始先通过两个 assertNotEqual 确保 testex1testex2 在初始化时候的 string_tab 属性和 numerical_tab 属性是不同的,然后再将 testex1string_tab 属性设置为 testex2string_tab 属性,人为的使 testex1testex2 在字符串表示上相同。然后再来测试 compute_numtab 的效果,即重新计算 testex1 的数值表示,然后再验证 testex1testex2 在修改后的属性是相同的
  • gpt3.5 填充的代码相较而言只考虑到了 testex1=testex2 后调用 compute_numtab 函数后两个 testex 的 string_tab 是否相同
  • LLM 自动生成的代码相较而言考虑的并不全面,虽然其足以验证基本的功能,但是没有考虑到验证 testex1testex2 的字符串表和数值表不相等,和验证验证 testex1testex2 修改后字符表相等,并不全面
  • 修改建议:给 gpt3.5 增加更详细的 prompt,比如告诉 gpt 需要在自动填充 assert 的时候考虑得更全面,充分的考虑到所有情况,避免出现因为过度考虑性能而忽视设计周密的情况。

# LLM 生成代码分析

定义:

  • 分析一:断言在验证的东西

  • 分析二:断言是否依赖于具体的 SUT (被测软件) 的输出及原因

  • 分析三:断言为何能生成

  1. for a, b in testcases:
                obj = cronex.CronExpression(a)
                self.assertTrue(b in repr(obj))
    <!--code10-->
    * 分析一:验证在修改表达式的字符串版本后,调用 `compute_numtab` 是否会更新静态触发值缓存。源代码在赋值和调用 `compute_numtab` 前后对 `string_tab` 和 `numerical_tab` 进行了检查,确保了这两个属性在修改和计算后的一致性。
    * 分析二:依赖——断言依赖于 `cronex.CronExpression` 对象的 `string_tab` 和 `numerical_tab` 属性。
    * 分析三:LLM只生成了`self.assertEqual(testex1.string_tab, testex2.string_tab)`,只验证了 `string_tab` 的一致性,没有检查 `numerical_tab` 的一致性。并且没有在赋值前检查 `string_tab` 和 `numerical_tab` 的初始状态是否不同,这可能会导致误判。LLM可能在生成过程中忽略了一些细节,没有充分理解 `numerical_tab` 属性在测试中的重要性。
  2. self.assertFalse(testex.check_trigger(now_tuple[:5]))
            testex.epoch = tuple(list(then[:5]) + [0])
            self.assertTrue(testex.check_trigger(now_tuple[:5]))
            self.assertTrue(testex.check_trigger(then[:5]))
    <!--code11-->
    * 分析一:验证在“小时”字段中的任意周期重复是否按预期工作。源代码通过 `self.assertFalse(testex.check_trigger(now_tuple[:5]))` 确保在设置 `epoch` 之前,当前时间戳不会触发 cron 表达式,这验证了初始状态的正确性,还验证了 `then[:5]`,确保在设置 `epoch` 后,两个不同时间点都能正确触发。
    * 分析二:断言依赖程序的输出,因为断言依赖于 `cronex.CronExpression.check_trigger` 的返回值。
    * 分析三:LLM可能未充分理解 cron 表达式与时间字段的关系,忽略了对 `now_tuple[:5]` 和 `then[:5]` 的精确验证。只知道验证分钟。
  3. for day in range(1, days_in_month + 1):
                            time_tuple = (year, month, day, 0, 0)
                            triggered = cron_expression.check_trigger(time_tuple)
                            self.assertEqual(should_trigger, triggered,
                                "%s: expected trigger to be %r for %r" %
                                (description, should_trigger, time_tuple)
                            )
    <!--code12-->
    * 分析一:验证解析原子表达式是否返回包含所有预期值的集合。即验证解析原子是否返回包含所有预期值的集合
    * 分析二:依赖,断言依赖于 `cronex.parse_atom` 的返回值。
    * 分析三:LLM通过函数名`test_parse_atom`和注释`Verify that parsing atoms returns sets containing all of the expected values.`分析得到该代码要求判断院子表达式与预期是否一致,从而生成准确的断言
  4. self.assertEqual(testex1.string_tab, testex2.string_tab)
            self.assertEqual(testex1.numerical_tab, testex2.numerical_tab)
            self.assertEqual(testex1.comment, testex2.comment)
            self.assertEqual(repr(testex1), str(testex1))
    <!--code13-->
    * 分析一:验证缩写的月份名称是否正确翻译为数字索引。通过 `repr` 方法进行比较,确保对象在**字符串**表示上的一致性。
    * 分析二:依赖,断言依赖于 `cronex.CronExpression` 的 `repr` 方法的输出。
    * 分析三:LLM从函数名`test_dom_substitution`知道是要分析名称是否被正确化为索引,但是LLM生成的代码直接比较两个对象而不是比较其字符串表示,并不准确。
  5. def test_dom_either_or_dow(self):
            """
            Verify that combining day of-the-month field and day-of-the-week field
            triggers on either condition matching the current day and that using a
            wild-card day of the month with a day of the week entry doesn't trigger
            every single day.
            """
            # Trigger should fire every Monday or on the 5th day of the month.
            testex = cronex.CronExpression("0 0 5 * mon")
            for e in xrange(1, 30):
                if e in (1, 5, 8, 15, 22, 29):
                    self.assertTrue(testex.check_trigger((2010, 11, e, 0, 0)))
                else:
                    self.assertFalse(testex.check_trigger((2010, 11, e, 0, 0)))
            # Trigger should fire every Wednesday at midnight.
            testex = cronex.CronExpression("0 0 * * wed")
            for d in xrange(1, 32):
                # In July of 2014, every Wednesday just happens to be on a day of
                # the month that's a multiple of 7.
                if not(d % 7):
                    self.assertTrue(testex.check_trigger((2010, 7, d, 0, 0)))
                else:
                    self.assertFalse(testex.check_trigger((2010, 7, d, 0, 0)))
    <!--code14-->
     
     
        * 分析一:验证在“星期几”字段中使用 `L` 是否总是在给定月份中相应星期的最后一次出现时触发。使用具体的年份和月份组合进行检查,确保逻辑覆盖所有可能的情况,包括每个月的最后一个星期六。
     
     
        * 分析二:依赖,断言依赖于 `cronex.CronExpression.check_trigger` 的返回值。
     
     
        * 分析三:LLM通过函数名`test_L_in_dow`知道了要验证的断言内容,但是LLM生成的代码使用`enumerate`和`tv`列表进行检查,虽然代码更简洁,但可能在一些边界情况下未能捕捉到潜在的问题。

# 一致率统计结果

共统计了 35 个断言,其中逻辑完全一致的 assert 共 8 个,不一致的 27 个,一致率为 22.9%