efj-parser 0.9.3__tar.gz → 0.9.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {efj_parser-0.9.3 → efj_parser-0.9.4}/PKG-INFO +1 -1
- {efj_parser-0.9.3 → efj_parser-0.9.4}/efj_parser/__init__.py +135 -91
- {efj_parser-0.9.3 → efj_parser-0.9.4}/efj_parser.egg-info/PKG-INFO +1 -1
- {efj_parser-0.9.3 → efj_parser-0.9.4}/setup.py +1 -1
- {efj_parser-0.9.3 → efj_parser-0.9.4}/tests/test_parser.py +321 -14
- {efj_parser-0.9.3 → efj_parser-0.9.4}/LICENSE +0 -0
- {efj_parser-0.9.3 → efj_parser-0.9.4}/README.md +0 -0
- {efj_parser-0.9.3 → efj_parser-0.9.4}/efj_parser/py.typed +0 -0
- {efj_parser-0.9.3 → efj_parser-0.9.4}/efj_parser.egg-info/SOURCES.txt +0 -0
- {efj_parser-0.9.3 → efj_parser-0.9.4}/efj_parser.egg-info/dependency_links.txt +0 -0
- {efj_parser-0.9.3 → efj_parser-0.9.4}/efj_parser.egg-info/top_level.txt +0 -0
- {efj_parser-0.9.3 → efj_parser-0.9.4}/pyproject.toml +0 -0
- {efj_parser-0.9.3 → efj_parser-0.9.4}/setup.cfg +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from enum import IntEnum
|
|
2
3
|
from typing import NamedTuple, Optional, Callable, Union, cast
|
|
3
4
|
import datetime as dt
|
|
4
5
|
|
|
@@ -77,7 +78,38 @@ class ValidationError(Exception):
|
|
|
77
78
|
|
|
78
79
|
class _VE(Exception):
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
class Code(IntEnum):
|
|
82
|
+
BAD_CREWLIST = 100
|
|
83
|
+
BAD_DATE = 110
|
|
84
|
+
BAD_FLAGS = 120
|
|
85
|
+
BAD_ROLE_FLAGS = 121
|
|
86
|
+
BAD_CONDITION_FLAGS = 122
|
|
87
|
+
BAD_SYNTAX = 130
|
|
88
|
+
BAD_TIME = 140
|
|
89
|
+
MISSING_AIRCRAFT = 200
|
|
90
|
+
MISSING_CAPTAIN = 210
|
|
91
|
+
MISSING_DATE = 220
|
|
92
|
+
MISSING_DEST = 230
|
|
93
|
+
MISSING_ORIGIN = 240
|
|
94
|
+
|
|
95
|
+
message_lookup = {
|
|
96
|
+
Code.BAD_CREWLIST: "Invalid crew list",
|
|
97
|
+
Code.BAD_DATE: "Invalid date format",
|
|
98
|
+
Code.BAD_FLAGS: "Invalid flags",
|
|
99
|
+
Code.BAD_ROLE_FLAGS: "Invalid role flags",
|
|
100
|
+
Code.BAD_CONDITION_FLAGS: "Invalid flight condition flags",
|
|
101
|
+
Code.BAD_SYNTAX: "Invalid syntax",
|
|
102
|
+
Code.BAD_TIME: "Invalid time format",
|
|
103
|
+
Code.MISSING_AIRCRAFT: "Prior aircraft specifier required",
|
|
104
|
+
Code.MISSING_AIRCRAFT: "No Captain specified",
|
|
105
|
+
Code.MISSING_DATE: "Prior date specifier required",
|
|
106
|
+
Code.MISSING_DEST: "No destination airfield specified",
|
|
107
|
+
Code.MISSING_ORIGIN: "No origin airfield specified",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
def __init__(self, code: Code):
|
|
111
|
+
self.code = code
|
|
112
|
+
self.message = _VE.message_lookup.get(code, "Unknown error")
|
|
81
113
|
|
|
82
114
|
|
|
83
115
|
ParseRet = Union[dt.date, Duty, Aircraft, list[Crewmember], Sector, str]
|
|
@@ -88,56 +120,76 @@ Flags = tuple[Flag, ...]
|
|
|
88
120
|
|
|
89
121
|
class Parser():
|
|
90
122
|
"""Parser for electronic Flight Journal (eFJ) files."""
|
|
123
|
+
|
|
124
|
+
__RE_SECTOR = re.compile(
|
|
125
|
+
r"""(\w*)/(\w*) # origin/dest (both optional)
|
|
126
|
+
\s+(\d{4})/(\d{4}) # times
|
|
127
|
+
(?>\s+([^#]+))? # flags
|
|
128
|
+
(?>\s*\#(.*))? # comment
|
|
129
|
+
""", re.VERBOSE)
|
|
130
|
+
__RE_CREWLIST = re.compile(r"\{([^}]*)}")
|
|
131
|
+
__RE_DATE = re.compile(r"(\d{4}-\d{2}-\d{2})")
|
|
132
|
+
__RE_AIRCRAFT = re.compile(
|
|
133
|
+
r"""(?>([-\w]+) # registration
|
|
134
|
+
\s*:\s*
|
|
135
|
+
([-\w]+)) # type
|
|
136
|
+
(?:\s*:\s*
|
|
137
|
+
(mc|spse|spme))? # optional class""", re.VERBOSE)
|
|
138
|
+
__RE_DUTY = re.compile(
|
|
139
|
+
r"""(\d{4})/(\d{4}) # times
|
|
140
|
+
(?>\s+([^#]+))? # flags
|
|
141
|
+
(?>\s*\#(.*))? # comment
|
|
142
|
+
""", re.VERBOSE)
|
|
143
|
+
__RE_SHORTDATE = re.compile(r"(\++)")
|
|
144
|
+
__RE_COMMENT = re.compile(r"#(.*)")
|
|
145
|
+
|
|
91
146
|
def __init__(self) -> None:
|
|
92
147
|
self.date: Optional[dt.date] = None
|
|
93
148
|
self.airports = Airports("", "")
|
|
94
149
|
self.aircraft = Aircraft("", "", "")
|
|
95
150
|
self.class_lookup: dict[str, str] = {}
|
|
96
|
-
self.captain = ""
|
|
97
151
|
self.crewlist: list[Crewmember] = []
|
|
98
152
|
|
|
99
153
|
def __parse_date(self, mo: re.Match) -> dt.date:
|
|
154
|
+
assert mo
|
|
100
155
|
try:
|
|
101
156
|
self.date = dt.date.fromisoformat(mo.group(1))
|
|
102
157
|
return self.date
|
|
103
158
|
except ValueError:
|
|
104
|
-
raise _VE(
|
|
159
|
+
raise _VE(_VE.Code.BAD_DATE)
|
|
105
160
|
|
|
106
161
|
def __parse_nextdate(self, mo: re.Match) -> dt.date:
|
|
162
|
+
assert mo
|
|
107
163
|
if not self.date:
|
|
108
|
-
raise _VE(
|
|
164
|
+
raise _VE(_VE.Code.MISSING_DATE)
|
|
109
165
|
self.date += dt.timedelta(len(mo.group(1)))
|
|
110
166
|
return self.date
|
|
111
167
|
|
|
112
168
|
def __parse_duty(self, mo: re.Match) -> Duty:
|
|
169
|
+
assert mo
|
|
113
170
|
if not self.date:
|
|
114
|
-
raise _VE(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
except ValueError:
|
|
119
|
-
raise _VE("Invalid time string")
|
|
120
|
-
dt_start = dt.datetime.combine(self.date, t_start)
|
|
121
|
-
dt_end = dt.datetime.combine(self.date, t_end)
|
|
122
|
-
if dt_end < dt_start:
|
|
123
|
-
dt_end += dt.timedelta(days=1)
|
|
124
|
-
duration = int((dt_end - dt_start).total_seconds() / 60)
|
|
171
|
+
raise _VE(_VE.Code.MISSING_DATE)
|
|
172
|
+
start_str, end_str, flags, comment = (
|
|
173
|
+
X.strip() if X else "" for X in mo.group(1, 2, 3, 4))
|
|
174
|
+
dt_start, duration = self.__parse_times(start_str, end_str)
|
|
125
175
|
ftl_correction = 0
|
|
126
176
|
unused_flags = []
|
|
127
|
-
for f in _split_flags(
|
|
177
|
+
for f in _split_flags(flags):
|
|
128
178
|
if f[0] == "r":
|
|
129
179
|
ftl_correction += f[1] if f[1] else duration
|
|
130
180
|
else:
|
|
131
181
|
unused_flags.append(f)
|
|
132
|
-
comment = mo.group(4)[1:].strip() if mo.group(4) else ""
|
|
133
182
|
return Duty(dt_start, duration, ftl_correction,
|
|
134
183
|
_join_flags(tuple(unused_flags)), comment)
|
|
135
184
|
|
|
136
185
|
def __parse_aircraft(self, mo: re.Match) -> Aircraft:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
class_
|
|
186
|
+
assert mo
|
|
187
|
+
reg, type_, class_ = (X.strip() if X else ""
|
|
188
|
+
for X in mo.group(1, 2, 3))
|
|
189
|
+
if class_:
|
|
190
|
+
self.class_lookup[type_] = class_
|
|
191
|
+
else:
|
|
192
|
+
class_ = self.class_lookup.get(type_, "")
|
|
141
193
|
self.aircraft = Aircraft(reg, type_, class_)
|
|
142
194
|
return self.aircraft
|
|
143
195
|
|
|
@@ -157,37 +209,24 @@ class Parser():
|
|
|
157
209
|
raise ValueError
|
|
158
210
|
self.crewlist.append(Crewmember(role, name))
|
|
159
211
|
except ValueError:
|
|
160
|
-
raise _VE(
|
|
161
|
-
captains = [X.name for X in self.crewlist if X.role == "CP"]
|
|
162
|
-
if captains:
|
|
163
|
-
self.captain = ", ".join(captains)
|
|
212
|
+
raise _VE(_VE.Code.BAD_CREWLIST)
|
|
164
213
|
return self.crewlist
|
|
165
214
|
|
|
166
|
-
def
|
|
167
|
-
if not self.date:
|
|
168
|
-
raise _VE("Sector with no preceding Date entry")
|
|
169
|
-
if not (self.aircraft.reg and self.aircraft.type_):
|
|
170
|
-
raise _VE("Sector with no preceding Aircraft entry")
|
|
171
|
-
if not mo.group(1) and not self.airports.dest: # Blank destination
|
|
172
|
-
raise _VE("Blank From without previous To")
|
|
173
|
-
if not mo.group(2) and not self.airports.origin: # Blank origin
|
|
174
|
-
raise _VE("Blank To without previous From")
|
|
175
|
-
|
|
176
|
-
def __parse_sector_times(
|
|
215
|
+
def __parse_times(
|
|
177
216
|
self, t_start: str, t_end: str
|
|
178
217
|
) -> tuple[dt.datetime, int]:
|
|
218
|
+
assert self.date
|
|
219
|
+
assert t_start and t_end
|
|
220
|
+
assert t_start.isnumeric() and t_end.isnumeric()
|
|
179
221
|
try:
|
|
180
222
|
ts = dt.time.fromisoformat(t_start) # Off blocks
|
|
181
223
|
te = dt.time.fromisoformat(t_end) # On blocks
|
|
182
|
-
except ValueError:
|
|
183
|
-
raise _VE(
|
|
184
|
-
|
|
185
|
-
duration = int(
|
|
186
|
-
(dt.datetime.combine(cast(dt.date, self.date), te) - start)
|
|
187
|
-
.total_seconds() // 60)
|
|
224
|
+
except (ValueError, TypeError):
|
|
225
|
+
raise _VE(_VE.Code.BAD_TIME)
|
|
226
|
+
duration = (te.hour - ts.hour) * 60 + (te.minute - ts.minute)
|
|
188
227
|
if duration < 0:
|
|
189
228
|
duration += 1440
|
|
190
|
-
return (
|
|
229
|
+
return (dt.datetime.combine(self.date, ts), duration)
|
|
191
230
|
|
|
192
231
|
def __parse_sector_flags(
|
|
193
232
|
self,
|
|
@@ -203,45 +242,53 @@ class Parser():
|
|
|
203
242
|
conditions.night)
|
|
204
243
|
return conditions, roles, landings, unused_flags
|
|
205
244
|
|
|
245
|
+
def __captain(self, roles, duration) -> str:
|
|
246
|
+
captains = ["Self"] if roles.p1 else []
|
|
247
|
+
if roles.p1 < duration:
|
|
248
|
+
for m in self.crewlist:
|
|
249
|
+
if m.role.upper() == "CP":
|
|
250
|
+
captains.append(m.name)
|
|
251
|
+
return ", ".join(captains)
|
|
252
|
+
|
|
206
253
|
def __parse_sector(self, mo: re.Match) -> Sector:
|
|
207
|
-
self.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
254
|
+
if not self.date:
|
|
255
|
+
raise _VE(_VE.Code.MISSING_DATE)
|
|
256
|
+
if not (self.aircraft.reg and self.aircraft.type_):
|
|
257
|
+
raise _VE(_VE.Code.MISSING_AIRCRAFT)
|
|
258
|
+
(origin, dest, start_str, end_str, flag_str, comment
|
|
259
|
+
) = (X.strip() if X else "" for X in mo.group(*range(1, 7)))
|
|
260
|
+
self.airports = Airports(
|
|
261
|
+
origin or self.airports.dest,
|
|
262
|
+
dest or self.airports.origin
|
|
263
|
+
)
|
|
264
|
+
if not self.airports.origin:
|
|
265
|
+
raise _VE(_VE.Code.MISSING_ORIGIN)
|
|
266
|
+
if not self.airports.dest:
|
|
267
|
+
raise _VE(_VE.Code.MISSING_DEST)
|
|
268
|
+
start, duration = self.__parse_times(start_str, end_str)
|
|
211
269
|
try:
|
|
212
|
-
flags = _split_flags(
|
|
270
|
+
flags = _split_flags(flag_str)
|
|
213
271
|
except ValueError:
|
|
214
|
-
raise _VE(
|
|
272
|
+
raise _VE(_VE.Code.BAD_FLAGS)
|
|
215
273
|
conditions, roles, landings, unused_flags = (
|
|
216
274
|
self.__parse_sector_flags(flags, duration))
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if not self.captain:
|
|
221
|
-
raise _VE("No Captain specified")
|
|
222
|
-
if roles.p1:
|
|
223
|
-
self.captain = f"Self, {self.captain}"
|
|
224
|
-
comment = ""
|
|
225
|
-
if mo.group(6): # Comment
|
|
226
|
-
comment = mo.group(6)[1:].strip()
|
|
275
|
+
captain = self.__captain(roles, duration)
|
|
276
|
+
if not captain:
|
|
277
|
+
raise _VE(_VE.Code.MISSING_CAPTAIN)
|
|
227
278
|
return Sector(
|
|
228
|
-
start,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
conditions,
|
|
232
|
-
landings,
|
|
233
|
-
self.aircraft,
|
|
234
|
-
self.airports,
|
|
235
|
-
self.captain,
|
|
236
|
-
_join_flags(unused_flags),
|
|
279
|
+
start, duration, roles, conditions,
|
|
280
|
+
landings, self.aircraft, self.airports,
|
|
281
|
+
captain, _join_flags(unused_flags),
|
|
237
282
|
comment,
|
|
238
283
|
tuple(self.crewlist))
|
|
239
284
|
|
|
240
285
|
def __parse_comment(self, mo: re.Match) -> str:
|
|
241
286
|
return mo.group(1) or ""
|
|
242
287
|
|
|
243
|
-
def parse(
|
|
244
|
-
|
|
288
|
+
def parse(
|
|
289
|
+
self, s: str,
|
|
290
|
+
hook: ParseHook = None
|
|
291
|
+
) -> tuple[tuple[Duty, ...], tuple[Sector, ...]]:
|
|
245
292
|
"""Extract duties and sectors from an eFJ string
|
|
246
293
|
|
|
247
294
|
:param s: A string containing data in eFJ format
|
|
@@ -257,21 +304,13 @@ class Parser():
|
|
|
257
304
|
duties: list[Duty] = []
|
|
258
305
|
sectors: list[Sector] = []
|
|
259
306
|
func_map = [
|
|
260
|
-
(
|
|
261
|
-
|
|
262
|
-
(
|
|
263
|
-
|
|
264
|
-
(
|
|
265
|
-
|
|
266
|
-
(
|
|
267
|
-
self.__parse_duty, "duty"),
|
|
268
|
-
(re.compile(r"([-\w]{2,10})(?>\s*:\s*)([-\w]+)"
|
|
269
|
-
r"(?:\s*:\s*(mc|spse|spme))?"),
|
|
270
|
-
self.__parse_aircraft, "aircraft"),
|
|
271
|
-
(re.compile(r"(\++)"),
|
|
272
|
-
self.__parse_nextdate, "short_date"),
|
|
273
|
-
(re.compile(r"#(.*)"),
|
|
274
|
-
self.__parse_comment, "comment"),
|
|
307
|
+
(Parser.__RE_SECTOR, self.__parse_sector, "sector"),
|
|
308
|
+
(Parser.__RE_CREWLIST, self.__parse_crewlist, "crewlist"),
|
|
309
|
+
(Parser.__RE_DATE, self.__parse_date, "date"),
|
|
310
|
+
(Parser.__RE_DUTY, self.__parse_duty, "duty"),
|
|
311
|
+
(Parser.__RE_AIRCRAFT, self.__parse_aircraft, "aircraft"),
|
|
312
|
+
(Parser.__RE_SHORTDATE, self.__parse_nextdate, "short_date"),
|
|
313
|
+
(Parser.__RE_COMMENT, self.__parse_comment, "comment"),
|
|
275
314
|
]
|
|
276
315
|
for c, line in enumerate(s.splitlines()):
|
|
277
316
|
line = line.strip()
|
|
@@ -288,7 +327,8 @@ class Parser():
|
|
|
288
327
|
hook and hook(line, c + 1, entry_type, cast(ParseRet, ret))
|
|
289
328
|
break
|
|
290
329
|
else:
|
|
291
|
-
raise
|
|
330
|
+
raise ValidationError(
|
|
331
|
+
c + 1, _VE(_VE.Code.BAD_SYNTAX).message, line)
|
|
292
332
|
if isinstance(ret, Duty):
|
|
293
333
|
duties.append(ret)
|
|
294
334
|
elif isinstance(ret, Sector):
|
|
@@ -303,13 +343,17 @@ def _split_flags(flag_str: str) -> Flags:
|
|
|
303
343
|
:returns: tuple of pairs like (("p2", 20), ("ln", 1), ("ins", None), ...)
|
|
304
344
|
:raises: ValueError if RHS of : is not convertible to integer
|
|
305
345
|
"""
|
|
306
|
-
fields = flag_str.
|
|
346
|
+
fields = flag_str.split()
|
|
347
|
+
if not fields:
|
|
348
|
+
return ()
|
|
307
349
|
return tuple((L[0], int(L[1])) if len(L) == 2 else (L[0], None)
|
|
308
350
|
for L in (X.split(":", 1) for X in fields))
|
|
309
351
|
|
|
310
352
|
|
|
311
353
|
def _join_flags(flags: Flags) -> tuple[str, ...]:
|
|
312
354
|
"""Join Flags into a tuple of str"""
|
|
355
|
+
if not flags:
|
|
356
|
+
return ()
|
|
313
357
|
return tuple(f"{X[0]}:{X[1]}" if X[1] else X[0] for X in flags)
|
|
314
358
|
|
|
315
359
|
|
|
@@ -364,12 +408,12 @@ def _process_roles(flags: Flags, duration: int) -> tuple[Roles, Flags]:
|
|
|
364
408
|
case "ins":
|
|
365
409
|
ins += f[1] if f[1] else duration
|
|
366
410
|
if ins > duration:
|
|
367
|
-
raise _VE(
|
|
411
|
+
raise _VE(_VE.Code.BAD_ROLE_FLAGS)
|
|
368
412
|
case _:
|
|
369
413
|
unused.append(f)
|
|
370
414
|
p1 = duration - (p1s + p2 + put + p0)
|
|
371
415
|
if p1 < 0:
|
|
372
|
-
raise _VE(
|
|
416
|
+
raise _VE(_VE.Code.BAD_ROLE_FLAGS)
|
|
373
417
|
return Roles(p1, p1s, p2, put, ins), tuple(unused)
|
|
374
418
|
|
|
375
419
|
|
|
@@ -388,11 +432,11 @@ def _process_conditions(flags: Flags, dur: int) -> tuple[Conditions, Flags]:
|
|
|
388
432
|
case "n":
|
|
389
433
|
night += f[1] if f[1] else dur
|
|
390
434
|
if night > dur:
|
|
391
|
-
raise _VE(
|
|
435
|
+
raise _VE(_VE.Code.BAD_CONDITION_FLAGS)
|
|
392
436
|
case "v":
|
|
393
437
|
vfr += f[1] if f[1] else dur
|
|
394
438
|
if vfr > dur:
|
|
395
|
-
raise _VE(
|
|
439
|
+
raise _VE(_VE.Code.BAD_CONDITION_FLAGS)
|
|
396
440
|
case _:
|
|
397
441
|
unused.append(f)
|
|
398
442
|
return Conditions(night, dur - vfr), tuple(unused)
|
|
@@ -4,6 +4,134 @@ import datetime as dt
|
|
|
4
4
|
import efj_parser as efj
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
class TestRegexp(unittest.TestCase):
|
|
8
|
+
|
|
9
|
+
def test_shortdate(self):
|
|
10
|
+
f = efj.Parser._Parser__RE_SHORTDATE.fullmatch
|
|
11
|
+
# Next day
|
|
12
|
+
self.assertEqual(f("+").group(1), "+")
|
|
13
|
+
# Next next next day
|
|
14
|
+
self.assertEqual(f("+++").group(1), "+++")
|
|
15
|
+
# Can only be +
|
|
16
|
+
self.assertIsNone(f("++-+"))
|
|
17
|
+
# Empty gives None
|
|
18
|
+
self.assertIsNone(f(""))
|
|
19
|
+
|
|
20
|
+
def test_date(self):
|
|
21
|
+
f = efj.Parser._Parser__RE_DATE.fullmatch
|
|
22
|
+
self.assertEqual(f("2025-01-01").group(1), "2025-01-01")
|
|
23
|
+
# Must be ISO format
|
|
24
|
+
self.assertIsNone(f("01/01/2025"))
|
|
25
|
+
# Bad dates are OK -- will be detected when processing
|
|
26
|
+
self.assertEqual(f("2025-21-01").group(1), "2025-21-01")
|
|
27
|
+
# Can't be anything on the end
|
|
28
|
+
self.assertIsNone(f("2025-01-01A"))
|
|
29
|
+
# ... or the start
|
|
30
|
+
self.assertIsNone(f("A2025-01-01"))
|
|
31
|
+
# Dashes are mandatory
|
|
32
|
+
self.assertIsNone(f("20250101"))
|
|
33
|
+
# Empty gives None
|
|
34
|
+
self.assertIsNone(f(""))
|
|
35
|
+
|
|
36
|
+
def test_aircraft(self):
|
|
37
|
+
f = efj.Parser._Parser__RE_AIRCRAFT.fullmatch
|
|
38
|
+
for class_ in ["mc", "spse", "spme"]:
|
|
39
|
+
self.assertEqual(
|
|
40
|
+
f("G-ABCD:A320:" + class_).group(1, 2, 3),
|
|
41
|
+
("G-ABCD", "A320", class_)
|
|
42
|
+
)
|
|
43
|
+
# Other classes are no good
|
|
44
|
+
self.assertIsNone(f("G-ABCD:A320:mcme"))
|
|
45
|
+
# No class is fine
|
|
46
|
+
self.assertEqual(
|
|
47
|
+
f("G-ABCD:A320").group(1, 2, 3),
|
|
48
|
+
("G-ABCD", "A320", None)
|
|
49
|
+
)
|
|
50
|
+
# ... although trailing colon not allowed
|
|
51
|
+
self.assertIsNone(f("G-ABCD:A320:"))
|
|
52
|
+
# Space around colons is fine
|
|
53
|
+
self.assertEqual(
|
|
54
|
+
f("G-ABCD : A320 : mc").group(1, 2, 3),
|
|
55
|
+
("G-ABCD", "A320", "mc")
|
|
56
|
+
)
|
|
57
|
+
# Empty gives None
|
|
58
|
+
self.assertIsNone(f(""))
|
|
59
|
+
|
|
60
|
+
def test_comment(self):
|
|
61
|
+
f = efj.Parser._Parser__RE_COMMENT.fullmatch
|
|
62
|
+
self.assertEqual(f("# my comment").group(1), " my comment")
|
|
63
|
+
# Multiple #
|
|
64
|
+
self.assertEqual(f("## my #comment").group(1), "# my #comment")
|
|
65
|
+
# Empty comment
|
|
66
|
+
self.assertEqual(f("#").group(1), "")
|
|
67
|
+
# Empty
|
|
68
|
+
self.assertIsNone(f(""))
|
|
69
|
+
|
|
70
|
+
def test_crew(self):
|
|
71
|
+
f = efj.Parser._Parser__RE_CREWLIST.fullmatch
|
|
72
|
+
self.assertEqual(f("{ cp: Pugwash, fo: Bloggs}").group(1),
|
|
73
|
+
" cp: Pugwash, fo: Bloggs")
|
|
74
|
+
# Double close shouldn't match
|
|
75
|
+
self.assertIsNone(f("{ cp: Pugwash, fo: Bloggs}}"))
|
|
76
|
+
# Otherwise pretty much anything goes -- errors detected later
|
|
77
|
+
self.assertEqual(f("{{sdkfjl cp: :fo}").group(1),
|
|
78
|
+
"{sdkfjl cp: :fo")
|
|
79
|
+
# Empty
|
|
80
|
+
self.assertIsNone(f(""))
|
|
81
|
+
|
|
82
|
+
def test_duty(self):
|
|
83
|
+
f = efj.Parser._Parser__RE_DUTY.fullmatch
|
|
84
|
+
# just times
|
|
85
|
+
self.assertEqual(f("1000/1100").group(1, 2, 3, 4),
|
|
86
|
+
("1000", "1100", None, None))
|
|
87
|
+
# with a comment
|
|
88
|
+
self.assertEqual(f("1000/1100#test").group(1, 2, 3, 4),
|
|
89
|
+
("1000", "1100", None, "test"))
|
|
90
|
+
# with flags
|
|
91
|
+
self.assertEqual(f("1000/1100 r a:10").group(1, 2, 3, 4),
|
|
92
|
+
("1000", "1100", "r a:10", None))
|
|
93
|
+
# comment and flags
|
|
94
|
+
self.assertEqual(f("1000/1100 r:30 # test").group(1, 2, 3, 4),
|
|
95
|
+
("1000", "1100", "r:30 ", " test"))
|
|
96
|
+
# empty
|
|
97
|
+
self.assertIsNone(f(""))
|
|
98
|
+
# flag without space
|
|
99
|
+
self.assertIsNone(f("1000/1100r "))
|
|
100
|
+
# times with suffix
|
|
101
|
+
self.assertIsNone(f("1000z/1100z "))
|
|
102
|
+
# letters
|
|
103
|
+
self.assertIsNone(f("abcd/efgh"))
|
|
104
|
+
|
|
105
|
+
def test_sector(self):
|
|
106
|
+
f = efj.Parser._Parser__RE_SECTOR.fullmatch
|
|
107
|
+
# simplest
|
|
108
|
+
self.assertEqual(
|
|
109
|
+
f("BRS/FNC 1000/1300").group(1, 2, 3, 4, 5, 6),
|
|
110
|
+
("BRS", "FNC", "1000", "1300", None, None))
|
|
111
|
+
# flags
|
|
112
|
+
self.assertEqual(
|
|
113
|
+
f("BRS/FNC 1000/1300 n:30 m").group(1, 2, 3, 4, 5, 6),
|
|
114
|
+
("BRS", "FNC", "1000", "1300", "n:30 m", None))
|
|
115
|
+
# comment
|
|
116
|
+
self.assertEqual(
|
|
117
|
+
f("BRS/FNC 1000/1300 # test").group(1, 2, 3, 4, 5, 6),
|
|
118
|
+
("BRS", "FNC", "1000", "1300", None, " test"))
|
|
119
|
+
# flags and comment
|
|
120
|
+
self.assertEqual(
|
|
121
|
+
f("BRS/FNC 1000/1300 m# test").group(1, 2, 3, 4, 5, 6),
|
|
122
|
+
("BRS", "FNC", "1000", "1300", "m", " test"))
|
|
123
|
+
# underscore
|
|
124
|
+
self.assertEqual(
|
|
125
|
+
f("my_house/FNC 1000/1300"). group(1, 2, 3, 4, 5, 6),
|
|
126
|
+
("my_house", "FNC", "1000", "1300", None, None))
|
|
127
|
+
# no underscore
|
|
128
|
+
self.assertIsNone(f("my house/FNC 1000/1300"))
|
|
129
|
+
# time zones
|
|
130
|
+
self.assertIsNone(f("BRS/FNC 1000z/1300z"))
|
|
131
|
+
# empty
|
|
132
|
+
self.assertIsNone(f(""))
|
|
133
|
+
|
|
134
|
+
|
|
7
135
|
class TestParser(unittest.TestCase):
|
|
8
136
|
|
|
9
137
|
def test_basic(self):
|
|
@@ -215,21 +343,21 @@ BRS/BFS 1100/1200 v n:30
|
|
|
215
343
|
efj.Parser().parse(data)
|
|
216
344
|
self.assertEqual(
|
|
217
345
|
str(e.exception),
|
|
218
|
-
"Line 1: [
|
|
346
|
+
"Line 1: [Invalid crew list] {just a string}")
|
|
219
347
|
with self.subTest("No name"):
|
|
220
348
|
data = "2024-01-22\n{CP:, FO:Bloggs}"
|
|
221
349
|
with self.assertRaises(efj.ValidationError) as e:
|
|
222
350
|
efj.Parser().parse(data)
|
|
223
351
|
self.assertEqual(
|
|
224
352
|
str(e.exception),
|
|
225
|
-
"Line 2: [
|
|
353
|
+
"Line 2: [Invalid crew list] {CP:, FO:Bloggs}")
|
|
226
354
|
with self.subTest("No comma"):
|
|
227
355
|
data = "{ CP: Bloggs1 FO:Bloggs2}"
|
|
228
356
|
with self.assertRaises(efj.ValidationError) as e:
|
|
229
357
|
efj.Parser().parse(data)
|
|
230
358
|
self.assertEqual(
|
|
231
359
|
str(e.exception),
|
|
232
|
-
"Line 1: [
|
|
360
|
+
"Line 1: [Invalid crew list]"
|
|
233
361
|
" { CP: Bloggs1 FO:Bloggs2}")
|
|
234
362
|
with self.subTest("No colon"):
|
|
235
363
|
data = "{just a, string}"
|
|
@@ -237,14 +365,14 @@ BRS/BFS 1100/1200 v n:30
|
|
|
237
365
|
efj.Parser().parse(data)
|
|
238
366
|
self.assertEqual(
|
|
239
367
|
str(e.exception),
|
|
240
|
-
"Line 1: [
|
|
368
|
+
"Line 1: [Invalid crew list] {just a, string}")
|
|
241
369
|
with self.subTest("Multi word role"):
|
|
242
370
|
data = "{just a: string}"
|
|
243
371
|
with self.assertRaises(efj.ValidationError) as e:
|
|
244
372
|
efj.Parser().parse(data)
|
|
245
373
|
self.assertEqual(
|
|
246
374
|
str(e.exception),
|
|
247
|
-
"Line 1: [
|
|
375
|
+
"Line 1: [Invalid crew list] {just a: string}")
|
|
248
376
|
|
|
249
377
|
def test_bad_date(self):
|
|
250
378
|
data = "2024-02-30"
|
|
@@ -252,7 +380,7 @@ BRS/BFS 1100/1200 v n:30
|
|
|
252
380
|
efj.Parser().parse(data)
|
|
253
381
|
self.assertEqual(
|
|
254
382
|
str(e.exception),
|
|
255
|
-
"Line 1: [
|
|
383
|
+
"Line 1: [Invalid date format] 2024-02-30")
|
|
256
384
|
|
|
257
385
|
def test_bad_nextdate(self):
|
|
258
386
|
data = "2024-02-01\n++-"
|
|
@@ -260,7 +388,7 @@ BRS/BFS 1100/1200 v n:30
|
|
|
260
388
|
efj.Parser().parse(data)
|
|
261
389
|
self.assertEqual(
|
|
262
390
|
str(e.exception),
|
|
263
|
-
"Line 2: [
|
|
391
|
+
"Line 2: [Invalid syntax] ++-")
|
|
264
392
|
|
|
265
393
|
def test_bad_duty(self):
|
|
266
394
|
with self.subTest("No preceding date"):
|
|
@@ -269,21 +397,21 @@ BRS/BFS 1100/1200 v n:30
|
|
|
269
397
|
efj.Parser().parse(data)
|
|
270
398
|
self.assertEqual(
|
|
271
399
|
str(e.exception),
|
|
272
|
-
"Line 1: [
|
|
400
|
+
"Line 1: [Prior date specifier required] 1000/1100")
|
|
273
401
|
with self.subTest("Bad time format"):
|
|
274
402
|
data = "2024-02-01\n2200/2400"
|
|
275
403
|
with self.assertRaises(efj.ValidationError) as e:
|
|
276
404
|
efj.Parser().parse(data)
|
|
277
405
|
self.assertEqual(
|
|
278
406
|
str(e.exception),
|
|
279
|
-
"Line 2: [Invalid time
|
|
407
|
+
"Line 2: [Invalid time format] 2200/2400")
|
|
280
408
|
with self.subTest("Hyphen instead of slash"):
|
|
281
409
|
data = "2024-02-01\n2200-2400"
|
|
282
410
|
with self.assertRaises(efj.ValidationError) as e:
|
|
283
411
|
efj.Parser().parse(data)
|
|
284
412
|
self.assertEqual(
|
|
285
413
|
str(e.exception),
|
|
286
|
-
"Line 2: [
|
|
414
|
+
"Line 2: [Invalid syntax] 2200-2400")
|
|
287
415
|
|
|
288
416
|
|
|
289
417
|
class TestSectorFlags (unittest.TestCase):
|
|
@@ -379,11 +507,11 @@ class TestSectorFlags (unittest.TestCase):
|
|
|
379
507
|
with self.subTest("Role duration > duration"):
|
|
380
508
|
with self.assertRaises(efj._VE) as ve:
|
|
381
509
|
efj._process_roles((("p1s", 2),), 1)
|
|
382
|
-
self.assertEqual(ve.exception.message, "
|
|
510
|
+
self.assertEqual(ve.exception.message, "Invalid role flags")
|
|
383
511
|
with self.subTest("Two untimed roles"):
|
|
384
512
|
with self.assertRaises(efj._VE) as ve:
|
|
385
513
|
efj._process_roles((("p1s", None), ("put", None)), 1)
|
|
386
|
-
self.assertEqual(ve.exception.message, "
|
|
514
|
+
self.assertEqual(ve.exception.message, "Invalid role flags")
|
|
387
515
|
with self.subTest("Instructor flag"):
|
|
388
516
|
self.assertEqual(
|
|
389
517
|
efj._process_roles((("ins", None),), 1),
|
|
@@ -426,12 +554,12 @@ class TestSectorFlags (unittest.TestCase):
|
|
|
426
554
|
with self.assertRaises(efj._VE) as ve:
|
|
427
555
|
efj._process_conditions((("v", 2),), 1),
|
|
428
556
|
self.assertEqual(ve.exception.message,
|
|
429
|
-
"
|
|
557
|
+
"Invalid flight condition flags")
|
|
430
558
|
with self.subTest("Night duration > duration"):
|
|
431
559
|
with self.assertRaises(efj._VE) as ve:
|
|
432
560
|
efj._process_conditions((("n", 2),), 1),
|
|
433
561
|
self.assertEqual(ve.exception.message,
|
|
434
|
-
"
|
|
562
|
+
"Invalid flight condition flags")
|
|
435
563
|
|
|
436
564
|
|
|
437
565
|
class TestUtility(unittest.TestCase):
|
|
@@ -449,3 +577,182 @@ class TestUtility(unittest.TestCase):
|
|
|
449
577
|
)
|
|
450
578
|
exp = ("p2", "put:20", "ln:1", "ins")
|
|
451
579
|
self.assertEqual(res, exp)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class TestPrivateMethods(unittest.TestCase):
|
|
583
|
+
|
|
584
|
+
def test_parse_times(self):
|
|
585
|
+
parser = efj.Parser()
|
|
586
|
+
if __debug__:
|
|
587
|
+
with self.assertRaises(AssertionError) as e:
|
|
588
|
+
efj.Parser._Parser__parse_times(parser, "1000", "1100")
|
|
589
|
+
parser.date = dt.date(2025, 1, 1)
|
|
590
|
+
ret = efj.Parser._Parser__parse_times(parser, "1000", "1100")
|
|
591
|
+
self.assertEqual(ret, (dt.datetime(2025, 1, 1, 10, 0), 60))
|
|
592
|
+
ret = efj.Parser._Parser__parse_times(parser, "2300", "0100")
|
|
593
|
+
self.assertEqual(ret, (dt.datetime(2025, 1, 1, 23, 0), 120))
|
|
594
|
+
with self.assertRaises(efj._VE) as e:
|
|
595
|
+
efj.Parser._Parser__parse_times(parser, "1000", "3100")
|
|
596
|
+
self.assertEqual(e.exception.code, efj._VE.Code.BAD_TIME)
|
|
597
|
+
if __debug__:
|
|
598
|
+
with self.assertRaises(AssertionError) as e:
|
|
599
|
+
efj.Parser._Parser__parse_times(parser, "abcd", "1100")
|
|
600
|
+
with self.assertRaises(AssertionError) as e:
|
|
601
|
+
efj.Parser._Parser__parse_times(parser, None, "1100")
|
|
602
|
+
if not __debug__:
|
|
603
|
+
with self.assertRaises(efj._VE) as e:
|
|
604
|
+
efj.Parser._Parser__parse_times(parser, "abcd", "1100")
|
|
605
|
+
self.assertEqual(e.exception.code, efj._VE.Code.BAD_TIME)
|
|
606
|
+
with self.assertRaises(efj._VE) as e:
|
|
607
|
+
efj.Parser._Parser__parse_times(parser, None, "1100")
|
|
608
|
+
self.assertEqual(e.exception.code, efj._VE.Code.BAD_TIME)
|
|
609
|
+
|
|
610
|
+
def test_parse_date(self):
|
|
611
|
+
parser = efj.Parser()
|
|
612
|
+
mo = efj.Parser._Parser__RE_DATE.fullmatch("2025-01-01")
|
|
613
|
+
self.assertEqual(
|
|
614
|
+
efj.Parser._Parser__parse_date(parser, mo),
|
|
615
|
+
dt.date(2025, 1, 1))
|
|
616
|
+
mo = efj.Parser._Parser__RE_DATE.fullmatch("2025-21-01")
|
|
617
|
+
with self.assertRaises(efj._VE) as e:
|
|
618
|
+
efj.Parser._Parser__parse_date(parser, mo)
|
|
619
|
+
self.assertEqual(e.exception.code, efj._VE.Code.BAD_DATE)
|
|
620
|
+
if __debug__:
|
|
621
|
+
with self.assertRaises(AssertionError):
|
|
622
|
+
efj.Parser._Parser__parse_date(parser, None)
|
|
623
|
+
|
|
624
|
+
def test_nextdate(self):
|
|
625
|
+
parser = efj.Parser()
|
|
626
|
+
parser.date = dt.date(2024, 12, 31)
|
|
627
|
+
mo = efj.Parser._Parser__RE_SHORTDATE.fullmatch("+")
|
|
628
|
+
efj.Parser._Parser__parse_nextdate(parser, mo)
|
|
629
|
+
self.assertEqual(parser.date, dt.date(2025, 1, 1))
|
|
630
|
+
mo = efj.Parser._Parser__RE_SHORTDATE.fullmatch("+++")
|
|
631
|
+
efj.Parser._Parser__parse_nextdate(parser, mo)
|
|
632
|
+
self.assertEqual(parser.date, dt.date(2025, 1, 4))
|
|
633
|
+
if __debug__:
|
|
634
|
+
with self.assertRaises(AssertionError):
|
|
635
|
+
efj.Parser._Parser__parse_nextdate(parser, None)
|
|
636
|
+
|
|
637
|
+
def test_duty(self):
|
|
638
|
+
parser = efj.Parser()
|
|
639
|
+
parser.date = dt.date(2024, 12, 31)
|
|
640
|
+
mo = efj.Parser._Parser__RE_DUTY.fullmatch("1000/1100")
|
|
641
|
+
res = efj.Parser._Parser__parse_duty(parser, mo)
|
|
642
|
+
expected = efj.Duty(dt.datetime(2024, 12, 31, 10, 0), 60)
|
|
643
|
+
self.assertEqual(res, expected)
|
|
644
|
+
mo = efj.Parser._Parser__RE_DUTY.fullmatch("1000/1100 r:30")
|
|
645
|
+
res = efj.Parser._Parser__parse_duty(parser, mo)
|
|
646
|
+
expected = expected._replace(ftl_correction=30)
|
|
647
|
+
self.assertEqual(res, expected)
|
|
648
|
+
mo = efj.Parser._Parser__RE_DUTY.fullmatch("1000/1100 r")
|
|
649
|
+
res = efj.Parser._Parser__parse_duty(parser, mo)
|
|
650
|
+
expected = expected._replace(ftl_correction=60)
|
|
651
|
+
self.assertEqual(res, expected)
|
|
652
|
+
mo = efj.Parser._Parser__RE_DUTY.fullmatch(
|
|
653
|
+
"1000/1100 r t1 t2:10 # comment")
|
|
654
|
+
res = efj.Parser._Parser__parse_duty(parser, mo)
|
|
655
|
+
expected = expected._replace(
|
|
656
|
+
extra_flags=("t1", "t2:10"), comment="comment")
|
|
657
|
+
self.assertEqual(res, expected)
|
|
658
|
+
mo = efj.Parser._Parser__RE_DUTY.fullmatch("1000/1100#comment")
|
|
659
|
+
res = efj.Parser._Parser__parse_duty(parser, mo)
|
|
660
|
+
expected = efj.Duty(
|
|
661
|
+
dt.datetime(2024, 12, 31, 10, 0), 60, 0, (), "comment")
|
|
662
|
+
self.assertEqual(res, expected)
|
|
663
|
+
if __debug__:
|
|
664
|
+
with self.assertRaises(AssertionError):
|
|
665
|
+
efj.Parser._Parser__parse_duty(parser, None)
|
|
666
|
+
|
|
667
|
+
def test_aircraft(self):
|
|
668
|
+
parser = efj.Parser()
|
|
669
|
+
mo = efj.Parser._Parser__RE_AIRCRAFT.fullmatch("G-ABCD:A320:mc")
|
|
670
|
+
res = efj.Parser._Parser__parse_aircraft(parser, mo)
|
|
671
|
+
exp = efj.Aircraft("G-ABCD", "A320", "mc")
|
|
672
|
+
self.assertEqual(res, exp)
|
|
673
|
+
mo = efj.Parser._Parser__RE_AIRCRAFT.fullmatch("GA-BC-DE : A-320-neo")
|
|
674
|
+
res = efj.Parser._Parser__parse_aircraft(parser, mo)
|
|
675
|
+
exp = efj.Aircraft("GA-BC-DE", "A-320-neo", "")
|
|
676
|
+
self.assertEqual(res, exp)
|
|
677
|
+
|
|
678
|
+
def test_crewlist(self):
|
|
679
|
+
parser = efj.Parser()
|
|
680
|
+
c = efj.Parser._Parser__RE_CREWLIST.fullmatch
|
|
681
|
+
f = efj.Parser._Parser__parse_crewlist
|
|
682
|
+
res = f(parser, c("{ cp: pugwash, fo: bloggs}"))
|
|
683
|
+
exp = [
|
|
684
|
+
efj.Crewmember("cp", "pugwash"),
|
|
685
|
+
efj.Crewmember("fo", "bloggs")
|
|
686
|
+
]
|
|
687
|
+
self.assertEqual(res, exp)
|
|
688
|
+
res = efj.Parser._Parser__parse_crewlist(parser, c("{}"))
|
|
689
|
+
self.assertEqual(res, [])
|
|
690
|
+
with self.assertRaises(efj._VE) as e:
|
|
691
|
+
f(parser, c("{ cp: fo: bloggs }"))
|
|
692
|
+
self.assertEqual(e.exception.code, efj._VE.Code.BAD_CREWLIST)
|
|
693
|
+
with self.assertRaises(efj._VE) as e:
|
|
694
|
+
f(parser, c("{ cp fo: bloggs }"))
|
|
695
|
+
self.assertEqual(e.exception.code, efj._VE.Code.BAD_CREWLIST)
|
|
696
|
+
with self.assertRaises(efj._VE) as e:
|
|
697
|
+
f(parser, c("{{sdkfjl cp: :fo}"))
|
|
698
|
+
self.assertEqual(e.exception.code, efj._VE.Code.BAD_CREWLIST)
|
|
699
|
+
|
|
700
|
+
def test_sector(self):
|
|
701
|
+
parser = efj.Parser()
|
|
702
|
+
default_sector = efj.Sector(
|
|
703
|
+
start=dt.datetime(2025, 1, 1, 10),
|
|
704
|
+
total=60,
|
|
705
|
+
roles=efj.Roles(p1=60),
|
|
706
|
+
conditions=efj.Conditions(ifr=60),
|
|
707
|
+
landings=efj.Landings(1, 0),
|
|
708
|
+
aircraft=efj.Aircraft("G-ABCD", "A320", ""),
|
|
709
|
+
airports=efj.Airports("BRS", "FNC"),
|
|
710
|
+
captain="Self",
|
|
711
|
+
extra_flags=(),
|
|
712
|
+
comment="",
|
|
713
|
+
crew=())
|
|
714
|
+
r = efj.Parser._Parser__RE_SECTOR.fullmatch
|
|
715
|
+
f = efj.Parser._Parser__parse_sector
|
|
716
|
+
# parse errors
|
|
717
|
+
parser.date = dt.date(2025, 1, 1)
|
|
718
|
+
with self.assertRaises(efj._VE) as e:
|
|
719
|
+
f(parser, r("BRS/FNC 1000/1100"))
|
|
720
|
+
self.assertEqual(e.exception.code, efj._VE.Code.MISSING_AIRCRAFT)
|
|
721
|
+
parser.date = None
|
|
722
|
+
parser.aircraft = efj.Aircraft("G-ABCD", "A320", "")
|
|
723
|
+
with self.assertRaises(efj._VE) as e:
|
|
724
|
+
f(parser, r("BRS/FNC 1000/1100"))
|
|
725
|
+
self.assertEqual(e.exception.code, efj._VE.Code.MISSING_DATE)
|
|
726
|
+
parser.date = dt.date(2025, 1, 1)
|
|
727
|
+
parser.airports = efj.Airports("", "FNC")
|
|
728
|
+
with self.assertRaises(efj._VE) as e:
|
|
729
|
+
f(parser, r("/ 1000/1100"))
|
|
730
|
+
self.assertEqual(e.exception.code, efj._VE.Code.MISSING_DEST)
|
|
731
|
+
parser.airports = efj.Airports("BRS", "")
|
|
732
|
+
with self.assertRaises(efj._VE) as e:
|
|
733
|
+
f(parser, r("/ 1000/1100"))
|
|
734
|
+
self.assertEqual(e.exception.code, efj._VE.Code.MISSING_ORIGIN)
|
|
735
|
+
with self.assertRaises(efj._VE) as e:
|
|
736
|
+
f(parser, r("BRS/FNC 1000/1100 n:a"))
|
|
737
|
+
self.assertEqual(e.exception.code, efj._VE.Code.BAD_FLAGS)
|
|
738
|
+
with self.assertRaises(efj._VE) as e:
|
|
739
|
+
f(parser, r("BRS/FNC 1000/1100 p1s"))
|
|
740
|
+
self.assertEqual(e.exception.code, efj._VE.Code.MISSING_CAPTAIN)
|
|
741
|
+
# successful parse
|
|
742
|
+
self.assertEqual(f(parser, r("BRS/FNC 1000/1100")), default_sector)
|
|
743
|
+
self.assertEqual(
|
|
744
|
+
f(parser, r("/BHX 1000/1100")),
|
|
745
|
+
default_sector._replace(airports=efj.Airports("FNC", "BHX")))
|
|
746
|
+
self.assertEqual(
|
|
747
|
+
f(parser, r("/ 1000/1100")),
|
|
748
|
+
default_sector._replace(airports=efj.Airports("BHX", "FNC")))
|
|
749
|
+
self.assertEqual(f(parser, r("BRS/FNC 1000/1100#test comment")),
|
|
750
|
+
default_sector._replace(comment="test comment"))
|
|
751
|
+
parser.crewlist = [efj.Crewmember(role="cp", name="bloggs"),]
|
|
752
|
+
self.assertEqual(f(parser, r("BRS/FNC 1000/1100 p1s v n")),
|
|
753
|
+
default_sector._replace(
|
|
754
|
+
roles=efj.Roles(p1=0, p1s=60),
|
|
755
|
+
landings=efj.Landings(day=0, night=1),
|
|
756
|
+
captain="bloggs",
|
|
757
|
+
crew=(efj.Crewmember("cp", "bloggs"),),
|
|
758
|
+
conditions=efj.Conditions(night=60)))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|