Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/extractTest.py
169659 views
1
#!/usr/bin/env python
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2009-2025 German Aerospace Center (DLR) and others.
4
# This program and the accompanying materials are made available under the
5
# terms of the Eclipse Public License 2.0 which is available at
6
# https://www.eclipse.org/legal/epl-2.0/
7
# This Source Code may also be made available under the following Secondary
8
# Licenses when the conditions for such availability set forth in the Eclipse
9
# Public License 2.0 are satisfied: GNU General Public License, version 2
10
# or later which is available at
11
# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
12
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
13
14
# @file extractTest.py
15
# @author Daniel Krajzewicz
16
# @author Jakob Erdmann
17
# @author Michael Behrisch
18
# @author Mirko Barthauer
19
# @date 2009-07-08
20
21
"""
22
Extract all files for a test case into a new dir.
23
It may copy more files than needed because it copies everything
24
that is mentioned in the config under copy_test_path.
25
"""
26
from __future__ import absolute_import
27
from __future__ import print_function
28
import os
29
import stat
30
import sys
31
from os.path import join
32
import glob
33
import shutil
34
import shlex
35
import subprocess
36
from collections import defaultdict
37
38
THIS_DIR = os.path.abspath(os.path.dirname(__file__))
39
SUMO_HOME = os.path.dirname(THIS_DIR)
40
sys.path.append(join(SUMO_HOME, "tools"))
41
42
import sumolib # noqa
43
44
# cannot use ':' because it is a component of absolute paths on windows
45
SOURCE_DEST_SEP = ';'
46
47
48
def get_options(args=None):
49
optParser = sumolib.options.ArgumentParser(usage="%(prog)s <options> [<test directory>[;<target_directory>]]*")
50
optParser.add_option("-o", "--output", category="output", default=".", help="send output to directory")
51
optParser.add_option("-f", "--file", category="input", help="read list of source and target dirs from")
52
optParser.add_option("-p", "--python-script", category="input",
53
help="name of a python script to generate for a batch run")
54
optParser.add_option("-i", "--intelligent-names", category="processing", dest="names", action="store_true",
55
default=False, help="generate cfg name from directory name")
56
optParser.add_option("-v", "--verbose", category="processing", action="store_true", default=False,
57
help="more information")
58
optParser.add_option("-a", "--application", category="processing", help="sets the application to be used")
59
optParser.add_option("-s", "--skip-configuration", category="processing", default=False, action="store_true",
60
help="skips creation of an application config from the options.app file")
61
optParser.add_option("-x", "--skip-validation", category="processing", default=False, action="store_true",
62
help="remove all options related to XML validation")
63
optParser.add_option("-d", "--no-subdir", category="processing", dest="noSubdir", action="store_true",
64
default=False, help="store test files directly in the output directory")
65
optParser.add_option("--depth", category="processing", type=int, default=1,
66
help="maximum depth when descending into testsuites")
67
optParser.add_option("test_dir", nargs="*", category="input", help="read list of source and target dirs from")
68
options = optParser.parse_args(args)
69
if not options.file and not options.test_dir:
70
optParser.error("Please specify either an input file or a test directory")
71
return options
72
73
74
def copy_merge(srcDir, dstDir, merge, exclude):
75
"""merge contents of srcDir recursively into dstDir"""
76
for dir, subdirs, files in os.walk(srcDir):
77
for ex in exclude:
78
if ex in subdirs:
79
subdirs.remove(ex)
80
dst = dir.replace(srcDir, dstDir)
81
if os.path.exists(dst) and not merge:
82
shutil.rmtree(dst)
83
if not os.path.exists(dst):
84
# print "creating dir '%s' as a copy of '%s'" % (dst, srcDir)
85
os.mkdir(dst)
86
for file in files:
87
# print("copying file '%s' to '%s'" % (join(dir, file), join(dst, file)))
88
shutil.copy(join(dir, file), join(dst, file))
89
90
91
def generateTargetName(baseDir, source):
92
return source[len(os.path.commonprefix([baseDir, source])):].replace(os.sep, '_')
93
94
95
def main(options):
96
targets = []
97
if options.file:
98
dirname = os.path.dirname(options.file)
99
for line in open(options.file):
100
line = line.strip()
101
if line and line[0] != '#':
102
ls = line.split(SOURCE_DEST_SEP) + [""]
103
ls[0] = join(dirname, ls[0])
104
ls[1] = join(dirname, ls[1])
105
targets.append(ls[:3])
106
for val in options.test_dir:
107
source_and_maybe_target = val.split(SOURCE_DEST_SEP) + ["", ""]
108
targets.append(source_and_maybe_target[:3])
109
depth = options.depth
110
while depth > 0:
111
newTargets = []
112
for source, target, app in targets:
113
source = os.path.realpath(source)
114
suites = glob.glob(os.path.join(source, "testsuite.*"))
115
if suites:
116
with open(suites[0]) as s:
117
for line in s:
118
line = line.strip()
119
if line and not line.startswith("#"):
120
if target == "" and not options.noSubdir:
121
target = generateTargetName(os.path.realpath(join(SUMO_HOME, "tests")), source)
122
newTargets.append((os.path.join(source, line), os.path.join(target, line), app))
123
else:
124
newTargets.append((source, target, app))
125
targets = newTargets
126
depth -= 1
127
128
if options.python_script:
129
if not os.path.exists(os.path.dirname(options.python_script)):
130
os.makedirs(os.path.dirname(options.python_script))
131
pyBatch = open(options.python_script, 'w')
132
pyBatch.write('''#!/usr/bin/env python
133
import subprocess, sys, os, multiprocessing
134
from os.path import abspath, dirname, join
135
THIS_DIR = abspath(dirname(__file__))
136
SUMO_HOME = os.environ.get("SUMO_HOME", dirname(dirname(THIS_DIR)))
137
os.environ["SUMO_HOME"] = SUMO_HOME
138
calls = [
139
''')
140
for source, target, app in targets:
141
optionsFiles = defaultdict(list)
142
configFiles = defaultdict(list)
143
potentials = defaultdict(list)
144
source = os.path.realpath(source)
145
curDir = source
146
if curDir[-1] == os.path.sep:
147
curDir = os.path.dirname(curDir)
148
while True:
149
for f in sorted(os.listdir(curDir)):
150
path = join(curDir, f)
151
if f not in potentials or os.path.isdir(path):
152
potentials[f].append(path)
153
if f.startswith("options."):
154
optionsFiles[f[8:]].append(path)
155
if f.startswith("config."):
156
configFiles[f[7:]].append(path)
157
if curDir == os.path.realpath(join(SUMO_HOME, "tests")) or curDir == os.path.dirname(curDir):
158
break
159
curDir = os.path.dirname(curDir)
160
if not configFiles:
161
print("Config not found for %s." % source, file=sys.stderr)
162
continue
163
if len(glob.glob(os.path.join(source, "testsuite.*"))) > 0:
164
print("Directory %s seems to contain a test suite." % source, file=sys.stderr)
165
continue
166
if app == "":
167
for v in configFiles.keys():
168
if "." not in v:
169
app = v
170
break
171
haveVariant = False
172
for variant in sorted(set(optionsFiles.keys()) | set(configFiles.keys())):
173
if options.application not in (None, "ALL", variant, variant.split(".")[-1]):
174
continue
175
if options.application is None and len(glob.glob(os.path.join(source, "*" + variant))) == 0:
176
if options.verbose:
177
print("ignoring variant %s for '%s'" % (variant, source))
178
continue
179
haveVariant = True
180
cfg = configFiles[variant] + configFiles[app]
181
if target == "" and not options.noSubdir:
182
target = generateTargetName(os.path.dirname(cfg[-1]), source)
183
testPath = os.path.abspath(join(options.output, target))
184
if not os.path.exists(testPath):
185
os.makedirs(testPath)
186
net = None
187
skip = False
188
appOptions = []
189
optFiles = optionsFiles[app] + ([] if variant == app else optionsFiles[variant])
190
for f in sorted(optFiles, key=lambda o: o.count(os.sep)):
191
newOptions = []
192
clearOptions = None
193
with open(f) as oFile:
194
for o in shlex.split(oFile.read()):
195
if skip:
196
skip = False
197
continue
198
if o == "--xml-validation" and options.skip_validation:
199
skip = True
200
continue
201
if o == "{CLEAR}":
202
appOptions = []
203
continue
204
if o == "{CLEAR":
205
clearOptions = []
206
continue
207
if clearOptions is not None:
208
if o[-1] == "}":
209
clearOptions.append(o[:-1])
210
numClear = len(clearOptions)
211
for idx in range(len(appOptions) - numClear + 1):
212
if appOptions[idx:idx+numClear] == clearOptions:
213
del appOptions[idx:idx+numClear]
214
clearOptions = None
215
break
216
else:
217
clearOptions.append(o)
218
continue
219
if o[0] == "-" and o in appOptions:
220
idx = appOptions.index(o)
221
if idx < len(appOptions) - 1 and appOptions[idx + 1][0] != "-":
222
del appOptions[idx:idx+2]
223
else:
224
del appOptions[idx:idx+1]
225
newOptions.append(o)
226
if "=" in o:
227
o = o.split("=")[-1]
228
if o[-8:] == ".net.xml":
229
net = o
230
appOptions += newOptions
231
nameBase = "test"
232
if options.names:
233
nameBase = os.path.basename(target)
234
if "." in variant:
235
nameBase += variant.split(".")[-1]
236
loadAllNets = False
237
for a in appOptions:
238
if "netdiff.py" in a or "remap_additionals.py" in a or "remap_network.py" in a:
239
loadAllNets = True
240
exclude = []
241
# gather copy_test_path exclusions
242
for configFile in cfg:
243
with open(configFile) as config:
244
for line in config:
245
entry = line.strip().split(':')
246
if entry and entry[0] == "test_data_ignore":
247
exclude.append(entry[1])
248
# copy test data from the tree
249
for configFile in cfg:
250
with open(configFile) as config:
251
for line in config:
252
entry = line.strip().split(':')
253
if entry and "copy_test_path" in entry[0] and entry[1] in potentials:
254
if "net" in app or loadAllNets or not net or entry[1][-8:] != ".net.xml" or entry[1] == net:
255
toCopy = potentials[entry[1]][0]
256
if os.path.isdir(toCopy):
257
# copy from least specific to most specific
258
merge = entry[0] == "copy_test_path_merge"
259
for toCopy in reversed(potentials[entry[1]]):
260
copy_merge(toCopy, join(testPath, os.path.basename(toCopy)), merge, exclude)
261
else:
262
shutil.copy2(toCopy, testPath)
263
if options.python_script:
264
if app == "netgen":
265
call = ['join(SUMO_HOME, "bin", "netgenerate")'] + ['"%s"' % a for a in appOptions]
266
elif app == "tools":
267
call = ['sys.executable'] + ['"%s"' % a for a in appOptions]
268
call[1] = 'join(SUMO_HOME, "%s")' % appOptions[0]
269
elif app == "complex":
270
call = ['sys.executable']
271
for a in appOptions:
272
if a.endswith(".py"):
273
if os.path.exists(join(testPath, os.path.basename(a))):
274
call.insert(1, '"./%s"' % os.path.basename(a))
275
else:
276
call.insert(1, 'join(SUMO_HOME, "%s")' % a)
277
else:
278
call.append('"%s"' % a)
279
else:
280
call = ['join(SUMO_HOME, "bin", "%s")' % app] + ['"%s"' % a for a in appOptions]
281
prefix = os.path.commonprefix((testPath, os.path.abspath(pyBatch.name)))
282
up = os.path.abspath(pyBatch.name)[len(prefix):].count(os.sep) * "../"
283
pyBatch.write(' (r"%s", [%s], r"%s%s"),\n' %
284
(testPath[len(prefix):], ', '.join(call), up, testPath[len(prefix):]))
285
if options.skip_configuration:
286
continue
287
oldWorkDir = os.getcwd()
288
os.chdir(testPath)
289
haveConfig = False
290
# look for python executable
291
pythonPath = os.environ["PYTHON"] if "PYTHON" in os.environ else os.environ.get("PYTHON_HOME", "python")
292
if os.path.isdir(pythonPath):
293
pythonPath = os.path.join(pythonPath, "python")
294
if app in ["dfrouter", "duarouter", "jtrrouter", "marouter", "netconvert",
295
"netgen", "netgenerate", "od2trips", "polyconvert", "sumo", "activitygen"]:
296
if app == "netgen":
297
# binary is now called differently but app still has the old name
298
app = "netgenerate"
299
if options.verbose:
300
print("calling %s for testPath '%s' with options '%s'" %
301
(sumolib.checkBinary(app), testPath, " ".join(appOptions)))
302
try:
303
haveConfig = subprocess.call([sumolib.checkBinary(app)] + appOptions +
304
['--save-configuration', '%s.%scfg' %
305
(nameBase, app[:4])]) == 0
306
except OSError:
307
print("Executable %s not found, generating shell scripts instead of config." % app, file=sys.stderr)
308
if not haveConfig:
309
appOptions.insert(0, '"$SUMO_HOME/bin/%s"' % app)
310
elif app == "tools":
311
for i, a in enumerate(appOptions):
312
if a.endswith(".py"):
313
del appOptions[i:i+1]
314
appOptions[0:0] = [pythonPath, '"$SUMO_HOME/%s"' % a]
315
break
316
if a.endswith(".jar"):
317
del appOptions[i:i+1]
318
appOptions[0:0] = ["java", "-jar", '"$SUMO_HOME/%s"' % a]
319
break
320
elif app == "complex":
321
for i, a in enumerate(appOptions):
322
if a.endswith(".py"):
323
if os.path.exists(join(testPath, os.path.basename(a))):
324
a = os.path.basename(a)
325
else:
326
a = '"$SUMO_HOME/%s"' % a
327
del appOptions[i:i+1]
328
appOptions[0:0] = [pythonPath, a]
329
break
330
if not haveConfig:
331
if options.verbose:
332
print("generating shell scripts for testPath '%s' with call '%s'" %
333
(testPath, " ".join(appOptions)))
334
cmd = [ao if " " not in ao else "'%s'" % ao for ao in appOptions]
335
with open(nameBase + ".sh", "w") as sh:
336
sh.write("#!/bin/bash\n")
337
sh.write(" ".join(cmd))
338
os.chmod(sh.name, os.stat(sh.name).st_mode | stat.S_IXUSR)
339
cmd = [o.replace("$SUMO_HOME", "%SUMO_HOME%") if " " not in o else '"%s"' % o for o in appOptions]
340
with open(nameBase + ".bat", "w") as bat:
341
bat.write(" ".join(cmd))
342
os.chdir(oldWorkDir)
343
if not haveVariant:
344
print("No suitable variant found for %s." % source, file=sys.stderr)
345
if options.python_script:
346
pyBatch.write("""]
347
procs = []
348
def check():
349
for d, p in procs:
350
if p.wait() != 0:
351
print("Error: '%s' failed for '%s'!" % (" ".join(getattr(p, "args", [str(p.pid)])), d))
352
sys.exit(1)
353
354
for dir, call, wd in calls:
355
procs.append((dir, subprocess.Popen(call, cwd=join(THIS_DIR, wd))))
356
if len(procs) == multiprocessing.cpu_count():
357
check()
358
procs = []
359
check()
360
""")
361
pyBatch.close()
362
os.chmod(pyBatch.name, os.stat(pyBatch.name).st_mode | stat.S_IXUSR)
363
364
365
if __name__ == "__main__":
366
main(get_options())
367
368