opencos-eda 0.2.56__py3-none-any.whl → 0.3.0__py3-none-any.whl

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.
Files changed (39) hide show
  1. opencos/_version.py +6 -3
  2. opencos/_waves_pkg.sv +34 -2
  3. opencos/commands/build.py +1 -0
  4. opencos/commands/export.py +1 -0
  5. opencos/commands/flist.py +1 -0
  6. opencos/commands/lec.py +5 -0
  7. opencos/commands/multi.py +4 -0
  8. opencos/commands/proj.py +1 -0
  9. opencos/commands/shell.py +4 -0
  10. opencos/commands/sim.py +47 -1
  11. opencos/commands/synth.py +4 -0
  12. opencos/deps/defaults.py +15 -7
  13. opencos/deps/deps_commands.py +84 -74
  14. opencos/deps_schema.py +3 -0
  15. opencos/eda.py +15 -2
  16. opencos/eda_base.py +61 -16
  17. opencos/eda_config.py +35 -1
  18. opencos/eda_config_defaults.yml +3 -1
  19. opencos/export_helper.py +5 -1
  20. opencos/tests/deps_files/command_order/DEPS.yml +11 -0
  21. opencos/tests/helpers.py +57 -19
  22. opencos/tests/test_deps_helpers.py +37 -25
  23. opencos/tests/test_eda.py +26 -60
  24. opencos/tools/cocotb.py +25 -0
  25. opencos/tools/modelsim_ase.py +65 -16
  26. opencos/tools/riviera.py +31 -6
  27. opencos/tools/slang_yosys.py +4 -1
  28. opencos/tools/verilator.py +28 -38
  29. opencos/tools/yosys.py +50 -20
  30. opencos/util.py +63 -12
  31. opencos/utils/vscode_helper.py +1 -1
  32. opencos/utils/vsim_helper.py +2 -3
  33. {opencos_eda-0.2.56.dist-info → opencos_eda-0.3.0.dist-info}/METADATA +2 -1
  34. {opencos_eda-0.2.56.dist-info → opencos_eda-0.3.0.dist-info}/RECORD +39 -39
  35. {opencos_eda-0.2.56.dist-info → opencos_eda-0.3.0.dist-info}/WHEEL +0 -0
  36. {opencos_eda-0.2.56.dist-info → opencos_eda-0.3.0.dist-info}/entry_points.txt +0 -0
  37. {opencos_eda-0.2.56.dist-info → opencos_eda-0.3.0.dist-info}/licenses/LICENSE +0 -0
  38. {opencos_eda-0.2.56.dist-info → opencos_eda-0.3.0.dist-info}/licenses/LICENSE.spdx +0 -0
  39. {opencos_eda-0.2.56.dist-info → opencos_eda-0.3.0.dist-info}/top_level.txt +0 -0
opencos/eda_base.py CHANGED
@@ -506,12 +506,11 @@ class Command: # pylint: disable=too-many-public-methods
506
506
  self.args[key].update(value)
507
507
 
508
508
  elif isinstance(cur_value, list):
509
- # if list, append (no duplicates)
509
+ # if list, append (allow duplicates)
510
510
  if isinstance(value, list):
511
511
  # new value also a list
512
512
  for x in value:
513
- if x not in self.args[key]:
514
- self.args[key].append(x)
513
+ self.args[key].append(x)
515
514
  elif value not in cur_value:
516
515
  self.args[key].append(value)
517
516
 
@@ -970,13 +969,16 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
970
969
  self.targets_dict = {} # key = targets that we've already processed in DEPS files
971
970
  self.last_added_source_file_inferred_top = ''
972
971
 
973
- self.has_dep_shell_commands = False
972
+ self.has_pre_compile_dep_shell_commands = False
973
+ self.has_post_tool_dep_shell_commands = False
974
974
 
975
975
 
976
976
  def run_dep_commands(self) -> None:
977
- '''Run shell/peakrdl style commands from DEPS files
977
+ '''Run shell/peakrdl style commands from DEPS files, this is peformed before
978
978
 
979
- These are deferred to maintain the deps ordering, and run in that order.
979
+ any tool compile step. These are deferred to maintain the deps ordering, and
980
+ run in that order. Note this will NOT run any DEPS command marked with
981
+ run-after-tool=True.
980
982
  '''
981
983
  self.run_dep_shell_commands()
982
984
  # Update any work_dir_add_srcs@ in our self.files, self.files_v, etc, b/c
@@ -986,17 +988,41 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
986
988
  self.update_non_source_files_in_work_dir()
987
989
 
988
990
 
989
- def run_dep_shell_commands(self) -> None:
990
- '''Specifically runs shell command from DEPS files'''
991
+ def run_post_tool_dep_commands(self) -> None:
992
+ '''Run shell style commands from DEPS files that have been marked with
993
+
994
+ run-after-tool=True. Note these are skipped if any args like
995
+ stop-before- or stop-after- are set.
996
+ '''
997
+
998
+ self.run_dep_shell_commands(filter_run_after_tool=True)
999
+
1000
+
1001
+ def run_dep_shell_commands( # pylint: disable=too-many-branches,too-many-locals
1002
+ self, filter_run_after_tool: bool = False
1003
+ ) -> None:
1004
+ '''Runs collected shell command from DEPS files.
1005
+
1006
+ There are two flavors of shell commands: with or without 'run-after-tool'
1007
+ set. The default is to run shell command before the compile step of any tool,
1008
+ by calling this method with default pre_compile=True before any tool runs
1009
+ (for generating code, etc). However, it may be useful to run shell commands
1010
+ after a tool is complete (check timing, coverage, etc).
1011
+ '''
991
1012
 
992
1013
  # Runs from self.args['work-dir']
993
1014
  all_cmds_lists = []
994
1015
 
995
1016
  log_fnames_count = {} # count per target_node.
996
1017
 
997
- for i, d in enumerate(self.dep_shell_commands):
1018
+ filtered_dep_shell_commands = []
1019
+ for value in self.dep_shell_commands:
1020
+ if value['attributes']['run-after-tool'] == filter_run_after_tool:
1021
+ filtered_dep_shell_commands.append(value)
1022
+
1023
+
1024
+ for i, d in enumerate(filtered_dep_shell_commands):
998
1025
  clist = util.ShellCommandList(d['exec_list'])
999
- run_from_work_dir = d['run_from_work_dir'] # default True
1000
1026
  log = clist.tee_fpath
1001
1027
  target_node = d["target_node"]
1002
1028
  if clist.tee_fpath is None:
@@ -1011,29 +1037,48 @@ class CommandDesign(Command): # pylint: disable=too-many-instance-attributes
1011
1037
  # (or tee name from DEPS.yml)
1012
1038
  [f'# command {i}: target: {d["target_path"]} : {target_node} --> {log}'],
1013
1039
  ]
1014
- if not run_from_work_dir:
1040
+ if not d['attributes']['run-from-work-dir']:
1015
1041
  all_cmds_lists.append([f'cd {d["target_path"]}'])
1016
1042
 
1017
1043
  # actual command (list or util.ShellCommandList)
1018
1044
  all_cmds_lists.append(clist)
1019
1045
 
1020
- if not run_from_work_dir:
1046
+ if not d['attributes']['run-from-work-dir']:
1021
1047
  all_cmds_lists.append([f'cd {os.path.abspath(self.args["work-dir"])}'])
1022
1048
 
1023
1049
  d['exec_list'] = clist # update to tee_fpath is set.
1024
1050
 
1025
1051
  if all_cmds_lists:
1052
+ if filter_run_after_tool:
1053
+ filename='post_tool_dep_shell_commands.sh'
1054
+ self.has_post_tool_dep_shell_commands = True
1055
+ else:
1056
+ filename='pre_compile_dep_shell_commands.sh'
1057
+ self.has_pre_compile_dep_shell_commands = True
1058
+
1026
1059
  util.write_shell_command_file(
1027
- dirpath=self.args['work-dir'], filename='pre_compile_dep_shell_commands.sh',
1060
+ dirpath=self.args['work-dir'], filename=filename,
1028
1061
  command_lists=all_cmds_lists
1029
1062
  )
1030
- self.has_dep_shell_commands = True
1031
1063
 
1032
- for i, d in enumerate(self.dep_shell_commands):
1064
+
1065
+ if all_cmds_lists and filter_run_after_tool and \
1066
+ any(self.args.get(x, False) for x in (
1067
+ "stop-before-compile",
1068
+ "stop-after-compile",
1069
+ "stop-after-elaborate"
1070
+ )):
1071
+ args_set = [key for key,value in self.args.items() if \
1072
+ key.startswith('stop-') and value]
1073
+ util.info(f'Skipping DEPS run-after-tool commands due to args {args_set}')
1074
+ util.debug(f'Skipped commands: {filtered_dep_shell_commands=}')
1075
+ return
1076
+
1077
+ for i, d in enumerate(filtered_dep_shell_commands):
1033
1078
  util.info(f'run_dep_shell_commands {i=}: {d=}')
1034
1079
  clist = util.ShellCommandList(d['exec_list'])
1035
1080
  tee_fpath=clist.tee_fpath
1036
- if d['run_from_work_dir']:
1081
+ if d['attributes']['run-from-work-dir']:
1037
1082
  run_from_dir = self.args['work-dir']
1038
1083
  else:
1039
1084
  # Run from the target's directory (not the `eda` caller $PWD)
opencos/eda_config.py CHANGED
@@ -47,7 +47,7 @@ class Defaults:
47
47
  'exe', 'handlers',
48
48
  'requires_env', 'requires_py', 'requires_cmd', 'requires_in_exe_path',
49
49
  'requires_vsim_helper', 'requires_vscode_extension',
50
- 'disable-tools-multi',
50
+ 'disable-tools-multi', 'disable-auto',
51
51
  ])
52
52
  supported_config_tool_keys = set([
53
53
  'defines',
@@ -158,6 +158,9 @@ def update_config_auto_tool_order_for_tool(tool: str, config: dict) -> str:
158
158
 
159
159
  user_exe = shutil.which(user_exe)
160
160
 
161
+ # try adding to $PATH if in form --tool=/path/to/exe
162
+ tool_try_add_to_path(tool)
163
+
161
164
  if tool not in config['auto_tools_order'][0]:
162
165
  return tool
163
166
  if not user_exe:
@@ -322,3 +325,34 @@ def write_eda_config_and_args(
322
325
  data['config'][k] = str(v)
323
326
 
324
327
  yaml_safe_writer(data=data, filepath=fullpath)
328
+
329
+
330
+ def tool_try_add_to_path(tool: str) -> None:
331
+ '''Since we support --tool=<name>=/path/to/bin/exe, attempt to prepend $PATH
332
+
333
+ with this information for this tool (which will nicely affect all subprocesses,
334
+ but not wreck our original shell).'''
335
+
336
+ if not tool or '=' not in tool:
337
+ return
338
+
339
+ name, exe = tool.split('=')
340
+ if os.path.isdir(name):
341
+ # Someone passes us --tool=<name>=/path/to/bin/ (did not have exe)
342
+ path = name
343
+ else:
344
+ # Someone passes us --tool=<name>=/path/to/bin/exe, remove the exe.
345
+ path, _ = os.path.split(exe)
346
+ if not path:
347
+ return
348
+
349
+ path = os.path.abspath(path)
350
+ if os.path.isdir(path):
351
+ paths = os.environ['PATH'].split(':')
352
+ if path not in paths:
353
+ os.environ['PATH'] = path + ':' + os.environ['PATH']
354
+ util.info(f'--tool={tool} has path information, prepending PATH with: {path}')
355
+ else:
356
+ util.info(f'--tool={tool} has path information, but {path} already in $PATH')
357
+ if exe and os.path.isfile(exe):
358
+ util.info(f'--tool={tool} has path information, using exe {shutil.which(exe)}')
@@ -229,6 +229,7 @@ tools:
229
229
  riviera:
230
230
  defines:
231
231
  OC_TOOL_RIVIERA: 1
232
+ RIVIERA: 1
232
233
  log-bad-strings:
233
234
  - "Error:"
234
235
  log-must-strings:
@@ -292,7 +293,7 @@ tools:
292
293
  - 3009 # 3009: [TSCALE] - Module 'myname' does not have a timeunit/timeprecision
293
294
  # specification in effect, but other modules do.
294
295
  simulate-waves-args: |
295
- -voptargs=+acc=bcgnprst
296
+ -voptargs=+acc=bcnprst
296
297
 
297
298
 
298
299
  iverilog:
@@ -516,6 +517,7 @@ auto_tools_order:
516
517
  sim: opencos.tools.iverilog.CommandSimIverilog
517
518
 
518
519
  cocotb:
520
+ disable-auto: True # do not allow this to run `eda sim` with --tool not set
519
521
  exe: python
520
522
  requires_cmd:
521
523
  - python -c "import cocotb; print(cocotb.__version__)"
opencos/export_helper.py CHANGED
@@ -286,7 +286,11 @@ def get_list_sv_included_files(
286
286
  # If we have more than N levels of `include hunting, then rethink this.
287
287
  # For example, some codebases would do their file dependencies as `include
288
288
  # as part of their header guards, which could be ~100 levels of nesting.
289
- for fname,traversed in sv_included_files_dict.items():
289
+
290
+ # make a copy of keys so we don't alter during traversal of the dict:
291
+ fnames = list(sv_included_files_dict.keys())
292
+ for fname in fnames:
293
+ traversed = sv_included_files_dict[fname]
290
294
  if not traversed:
291
295
  included_files_list = find_sv_included_files_within_file(
292
296
  filename=fname,
@@ -18,3 +18,14 @@ target_echo_hi_bye:
18
18
  target_test:
19
19
  deps: target_echo_hi_bye
20
20
  top: foo
21
+
22
+ target_test_with_post_tool_commands:
23
+ deps:
24
+ # In this test, we want to put a new command in the front of the ordered "deps" list,
25
+ # but with run-after-tool=true, so it should run after any of the normal pre-compile
26
+ # shell commands.
27
+ - commands:
28
+ - shell: echo "final goodbye"
29
+ run-after-tool: true
30
+ - target_echo_hi_bye
31
+ top: foo
opencos/tests/helpers.py CHANGED
@@ -13,6 +13,7 @@ from opencos import eda
13
13
  from opencos import deps_schema
14
14
  from opencos.utils.markup_helpers import yaml_safe_load
15
15
  from opencos.utils import status_constants
16
+ from opencos.utils.subprocess_helpers import subprocess_run_background
16
17
 
17
18
 
18
19
  def eda_wrap_is_sim_fail(rc: int, quiet: bool = False) -> bool:
@@ -39,6 +40,15 @@ def can_run_eda_command(*commands, config: dict) -> bool:
39
40
  if handler and getattr(handler, 'CHECK_REQUIRES', []):
40
41
  if not all(issubclass(handler, x) for x in getattr(handler, 'CHECK_REQUIRES', [])):
41
42
  return False
43
+
44
+ # We cannot run tools that have disable-auto set:
45
+ tool = getattr(handler, '_TOOL', '')
46
+ if handler and tool:
47
+ entry = config['auto_tools_order'][0].get(tool, {})
48
+ if entry and entry.get('disable-auto', False):
49
+ # This tool cannot automatically run our command.
50
+ return False
51
+
42
52
  runnable.append(True)
43
53
  return runnable and all(runnable)
44
54
 
@@ -148,6 +158,15 @@ class Helpers:
148
158
  DEFAULT_DIR = ''
149
159
  DEFAULT_LOG_DIR = os.getcwd()
150
160
  DEFAULT_LOG = os.path.join(DEFAULT_LOG_DIR, '.pytest.eda.log')
161
+
162
+ # How should the job run? subprocess? eda_wrap? eda.main?
163
+ # Note - if using eda.main, args like --debug will persist in opencos.util.args,
164
+ # so if you need those to be re-loaded, set RUN_IN_SUBPROCESS=True.
165
+ # Note - if you mess with os.enviorn, it may persist through subprocess.
166
+ RUN_IN_SUBPROCESS = True
167
+ USE_EDA_WRAP = True
168
+ PRESERVE_ENV = False
169
+
151
170
  def chdir(self):
152
171
  '''Changes directory to self.DEFAULT_DIR and removes eda.work, eda.export paths'''
153
172
  chdir_remove_work_dir('', self.DEFAULT_DIR)
@@ -164,7 +183,11 @@ class Helpers:
164
183
  ret = os.path.join(self.DEFAULT_LOG_DIR, right)
165
184
  return ret
166
185
 
167
- def log_it(self, command_str:str, logfile=None, use_eda_wrap=True) -> int:
186
+ def log_it(
187
+ self, command_str: str, logfile=None, use_eda_wrap: bool = True,
188
+ run_in_subprocess: bool = False,
189
+ preserve_env: bool = False
190
+ ) -> int:
168
191
  '''Replacement for calling eda.main or eda_wrap, when you want an internal logfile
169
192
 
170
193
  Usage:
@@ -174,6 +197,10 @@ class Helpers:
174
197
  Note this will run with --no-default-log to avoid a Windows problem with stomping
175
198
  on a log file.
176
199
  '''
200
+
201
+ if self.PRESERVE_ENV or preserve_env:
202
+ saved_env = os.environ.copy()
203
+
177
204
  logfile = self._resolve_logfile(logfile)
178
205
  rc = 50
179
206
 
@@ -182,13 +209,26 @@ class Helpers:
182
209
  # look at eda.work/{target}.sim/sim.log or xsim.log.
183
210
  print(f'{os.getcwd()=}')
184
211
  print(f'{command_str=}')
185
- with open(logfile, 'w', encoding='utf-8') as f:
186
- with redirect_stdout(f), redirect_stderr(f):
187
- if use_eda_wrap:
188
- rc = eda_wrap('--no-default-log', *(command_str.split()))
189
- else:
190
- rc = eda.main('--no-default-log', *(command_str.split()))
191
- print(f'Wrote: {os.path.abspath(logfile)=}')
212
+ if run_in_subprocess or self.RUN_IN_SUBPROCESS:
213
+ command_list = ['eda', '--no-default-log'] + command_str.split()
214
+ _, _, rc = subprocess_run_background(
215
+ work_dir=self.DEFAULT_DIR,
216
+ command_list=command_list,
217
+ background=True,
218
+ tee_fpath=logfile
219
+ )
220
+ else:
221
+ with open(logfile, 'w', encoding='utf-8') as f:
222
+ with redirect_stdout(f), redirect_stderr(f):
223
+ if use_eda_wrap or self.USE_EDA_WRAP:
224
+ rc = eda_wrap('--no-default-log', *(command_str.split()))
225
+ else:
226
+ rc = eda.main('--no-default-log', *(command_str.split()))
227
+ print(f'Wrote: {os.path.abspath(logfile)=}')
228
+
229
+ if self.PRESERVE_ENV or preserve_env:
230
+ os.environ = saved_env
231
+
192
232
  return rc
193
233
 
194
234
  def is_in_log(self, *want_str, logfile=None, windows_path_support=False):
@@ -207,33 +247,31 @@ class Helpers:
207
247
  '''gets all log lines with any of want_str args are in the logfile, or self.DEFAULT_LOG'''
208
248
  logfile = self._resolve_logfile(logfile)
209
249
  ret_list = []
210
- want_str0 = ' '.join(list(want_str))
211
- want_str1 = want_str0.replace('/', '\\')
212
250
  with open(logfile, encoding='utf-8') as f:
213
251
  for line in f.readlines():
214
- if want_str0 in line:
252
+ if any(x in line for x in list(want_str)):
215
253
  ret_list.append(line)
216
- elif windows_path_support and want_str1 in line:
254
+ elif windows_path_support and \
255
+ any(x.replace('/', '\\') in line for x in list(want_str)):
217
256
  ret_list.append(line)
218
257
  return ret_list
219
258
 
220
259
  def get_log_words_with(self, *want_str, logfile=None, windows_path_support=False):
221
- '''gets all log lines with any of *want_str within a single word
260
+ '''gets all log words with any of *want_str within a single word
222
261
  in the logfile or self.DEFAULT_LOG
223
262
  '''
224
263
  logfile = self._resolve_logfile(logfile)
225
264
  ret_list = []
226
- want_str0 = ' '.join(list(want_str))
227
- want_str1 = want_str0.replace('/', '\\')
228
265
  with open(logfile, encoding='utf-8') as f:
229
266
  for line in f.readlines():
230
- if want_str0 in line:
267
+ if any(x in line for x in list(want_str)):
231
268
  for word in line.split():
232
- if want_str0 in word:
269
+ if any(x in word for x in list(want_str)):
233
270
  ret_list.append(word)
234
- elif windows_path_support and want_str1 in line:
271
+ elif windows_path_support and \
272
+ any(x.replace('/', '\\') in line for x in list(want_str)):
235
273
  for word in line.split():
236
- if want_str1 in word:
274
+ if any(x.replace('/', '\\') in word for x in list(want_str)):
237
275
  ret_list.append(word)
238
276
 
239
277
  return ret_list
@@ -59,26 +59,26 @@ def test_get_all_targets_eda_multi():
59
59
 
60
60
  def test_parse_deps_shell_str__no_parse():
61
61
  line = 'some_file.sv'
62
- d = deps_commands.parse_deps_shell_str(line, '', '')
62
+ d = deps_commands.parse_deps_shell_str(line, '', '', attributes={})
63
63
  assert not d, f'{d=}'
64
64
 
65
65
  line = 'some_target:'
66
- d = deps_commands.parse_deps_shell_str(line, '', '')
66
+ d = deps_commands.parse_deps_shell_str(line, '', '', attributes={})
67
67
  assert not d, f'{d=}'
68
68
 
69
69
  line = ' csr@some_file.sv'
70
- d = deps_commands.parse_deps_shell_str(line, '', '')
70
+ d = deps_commands.parse_deps_shell_str(line, '', '', attributes={})
71
71
  assert not d, f'{d=}'
72
72
 
73
73
  def test_parse_deps_shell_str__cp():
74
74
  line = ' shell@ cp ./oclib_fifo_test.sv oclib_fifo_test_COPY.sv ;'
75
- d = deps_commands.parse_deps_shell_str(line, '', '')
75
+ d = deps_commands.parse_deps_shell_str(line, '', '', attributes={})
76
76
  assert d, f'{d=}'
77
77
  assert d['exec_list'] == ['cp', './oclib_fifo_test.sv', 'oclib_fifo_test_COPY.sv', ';'], f'{d=}'
78
78
 
79
79
  def test_parse_deps_shell_str__echo():
80
80
  line = ' shell@echo "hello world"'
81
- d = deps_commands.parse_deps_shell_str(line, '', '')
81
+ d = deps_commands.parse_deps_shell_str(line, '', '', attributes={})
82
82
  assert d, f'{d=}'
83
83
  assert d['exec_list'] == ['echo', '"hello', 'world"'], f'{d=}'
84
84
 
@@ -88,7 +88,9 @@ def test_parse_deps_shell_str__enable_filepath_replacement():
88
88
  module_dir = os.path.dirname(os.path.abspath(__file__))
89
89
  os.chdir(module_dir)
90
90
  line = 'shell@cp ../deps/deps_commands.py .pytest.copied.py'
91
- d = deps_commands.parse_deps_shell_str(line, target_path='./', target_node='foo_target')
91
+ d = deps_commands.parse_deps_shell_str(
92
+ line, target_path='./', target_node='foo_target', attributes={}
93
+ )
92
94
  assert d, f'{d=}'
93
95
  spath = os.path.abspath(os.path.join('..', 'deps', 'deps_commands.py'))
94
96
  assert d['exec_list'] == ['cp', spath, '.pytest.copied.py'], f'{d=}'
@@ -100,8 +102,10 @@ def test_parse_deps_shell_str__disable_filepath_replacement():
100
102
  module_dir = os.path.dirname(os.path.abspath(__file__))
101
103
  os.chdir(module_dir)
102
104
  line = 'shell@cp ../deps/deps_commands.py .pytest.copied.py'
103
- d = deps_commands.parse_deps_shell_str(line, target_path='./', target_node='foo_target',
104
- enable_filepath_subst_target_dir=False)
105
+ d = deps_commands.parse_deps_shell_str(
106
+ line, target_path='./', target_node='foo_target',
107
+ attributes={'filepath-subst-target-dir': False}
108
+ )
105
109
  assert d, f'{d=}'
106
110
  assert d['exec_list'] == ['cp', '../deps/deps_commands.py', '.pytest.copied.py'], f'{d=}'
107
111
  assert d['target_node'] == 'foo_target'
@@ -112,8 +116,10 @@ def test_parse_deps_shell_str__enable_dirpath_replacement():
112
116
  module_dir = os.path.dirname(os.path.abspath(__file__))
113
117
  os.chdir(module_dir)
114
118
  line = 'shell@ls -ltr ./'
115
- d = deps_commands.parse_deps_shell_str(line, target_path='./', target_node='foo_target',
116
- enable_dirpath_subst_target_dir=True)
119
+ d = deps_commands.parse_deps_shell_str(
120
+ line, target_path='./', target_node='foo_target',
121
+ attributes={'dirpath-subst-target-dir': True}
122
+ )
117
123
  assert d, f'{d=}'
118
124
  assert d['exec_list'] == ['ls', '-ltr', os.path.abspath('./')], f'{d=}'
119
125
  assert d['target_node'] == 'foo_target'
@@ -125,7 +131,10 @@ def test_parse_deps_shell_str__disable_dirpath_replacement():
125
131
  module_dir = os.path.dirname(os.path.abspath(__file__))
126
132
  os.chdir(module_dir)
127
133
  line = 'shell@ls -ltr ./'
128
- d = deps_commands.parse_deps_shell_str(line, target_path='./', target_node='foo_target')
134
+ d = deps_commands.parse_deps_shell_str(
135
+ line, target_path='./', target_node='foo_target',
136
+ attributes={}
137
+ )
129
138
  assert d, f'{d=}'
130
139
  assert d['exec_list'] == ['ls', '-ltr', './'], f'{d=}'
131
140
  assert d['target_node'] == 'foo_target'
@@ -134,26 +143,26 @@ def test_parse_deps_shell_str__disable_dirpath_replacement():
134
143
 
135
144
  def test_parse_deps_work_dir_add_srcs__no_parse():
136
145
  line = 'some_file.sv'
137
- d = deps_commands.parse_deps_work_dir_add_srcs(line, '', '')
146
+ d = deps_commands.parse_deps_work_dir_add_srcs(line, '', '', {})
138
147
  assert not d, f'{d=}'
139
148
 
140
149
  line = 'some_target:'
141
- d = deps_commands.parse_deps_work_dir_add_srcs(line, '', '')
150
+ d = deps_commands.parse_deps_work_dir_add_srcs(line, '', '', {})
142
151
  assert not d, f'{d=}'
143
152
 
144
153
  line = ' csr@some_file.sv'
145
- d = deps_commands.parse_deps_work_dir_add_srcs(line, '', '')
154
+ d = deps_commands.parse_deps_work_dir_add_srcs(line, '', '', {})
146
155
  assert not d, f'{d=}'
147
156
 
148
157
  def test_parse_deps_work_dir_add_srcs__single_file():
149
158
  line = ' work_dir_add_srcs@ single_file.txt'
150
- d = deps_commands.parse_deps_work_dir_add_srcs(line, '', '')
159
+ d = deps_commands.parse_deps_work_dir_add_srcs(line, '', '', {})
151
160
  assert d, f'{d=}'
152
161
  assert d['file_list'] == ['single_file.txt']
153
162
 
154
163
  def test_parse_deps_work_dir_add_srcs__several_file():
155
164
  line = ' work_dir_add_srcs@ single_file.txt another.sv gen-verilog/mine.v ./gen-vhdl/wordy.vhdl'
156
- d = deps_commands.parse_deps_work_dir_add_srcs(line, '', '')
165
+ d = deps_commands.parse_deps_work_dir_add_srcs(line, '', '', {})
157
166
  assert d, f'{d=}'
158
167
  assert d['file_list'] == [
159
168
  'single_file.txt', 'another.sv', 'gen-verilog/mine.v', './gen-vhdl/wordy.vhdl'
@@ -162,34 +171,37 @@ def test_parse_deps_work_dir_add_srcs__several_file():
162
171
 
163
172
  def test_parse_deps_peakrdl__no_parse():
164
173
  line = 'some_file.sv'
165
- d = deps_commands.parse_deps_peakrdl(line, '', '')
174
+ d = deps_commands.parse_deps_peakrdl(line, '', '', {})
166
175
  assert not d, f'{d=}'
167
176
 
168
177
  line = 'some_target:'
169
- d = deps_commands.parse_deps_peakrdl(line, '', '')
178
+ d = deps_commands.parse_deps_peakrdl(line, '', '', {})
170
179
  assert not d, f'{d=}'
171
180
 
172
181
  line = ' csr@some_file.sv'
173
- d = deps_commands.parse_deps_peakrdl(line, '', '')
182
+ d = deps_commands.parse_deps_peakrdl(line, '', '', {})
174
183
  assert not d, f'{d=}'
175
184
 
176
185
  def test_parse_deps_peakrdl__with_top():
177
186
  line = ' peakrdl@ --cpuif axi4-lite-flat --top my_fancy_csrs ./my_csrs.rdl'
178
- d = deps_commands.parse_deps_peakrdl(line, '', '')
187
+ d = deps_commands.parse_deps_peakrdl(line, '', '', {})
179
188
  assert d, f'{d=}'
180
189
  assert len(d['shell_commands_list']) > 0
181
- assert d['work_dir_add_srcs']['file_list'] == ['peakrdl/my_fancy_csrs_pkg.sv', 'peakrdl/my_fancy_csrs.sv']
190
+ assert d['work_dir_add_srcs']['file_list'] == ['peakrdl/my_fancy_csrs_pkg.sv',
191
+ 'peakrdl/my_fancy_csrs.sv']
182
192
 
183
193
  def test_parse_deps_peakrdl__with_top2():
184
194
  line = ' peakrdl@ --cpuif axi4-lite-flat --top=my_fancy_csrs ./my_csrs.rdl'
185
- d = deps_commands.parse_deps_peakrdl(line, '', '')
195
+ d = deps_commands.parse_deps_peakrdl(line, '', '', {})
186
196
  assert d, f'{d=}'
187
197
  assert len(d['shell_commands_list']) > 0
188
- assert d['work_dir_add_srcs']['file_list'] == ['peakrdl/my_fancy_csrs_pkg.sv', 'peakrdl/my_fancy_csrs.sv']
198
+ assert d['work_dir_add_srcs']['file_list'] == ['peakrdl/my_fancy_csrs_pkg.sv',
199
+ 'peakrdl/my_fancy_csrs.sv']
189
200
 
190
201
  def test_parse_deps_peakrdl__infer_top():
191
202
  line = ' peakrdl@ --cpuif axi4-lite-flat ./my_csrs.rdl'
192
- d = deps_commands.parse_deps_peakrdl(line, '', '')
203
+ d = deps_commands.parse_deps_peakrdl(line, '', '', {})
193
204
  assert d, f'{d=}'
194
205
  assert len(d['shell_commands_list']) > 0
195
- assert d['work_dir_add_srcs']['file_list'] == ['peakrdl/my_csrs_pkg.sv', 'peakrdl/my_csrs.sv']
206
+ assert d['work_dir_add_srcs']['file_list'] == ['peakrdl/my_csrs_pkg.sv',
207
+ 'peakrdl/my_csrs.sv']
opencos/tests/test_eda.py CHANGED
@@ -20,7 +20,6 @@ and should be more gracefully handled.
20
20
  import os
21
21
  import shutil
22
22
  import subprocess
23
- from contextlib import redirect_stdout, redirect_stderr
24
23
 
25
24
  import pytest
26
25
 
@@ -107,6 +106,7 @@ class TestsRequiresVerilator( # pylint: disable=too-many-public-methods
107
106
  assert rc == 0
108
107
 
109
108
 
109
+
110
110
  def test_args_sim_tool_with_path(self):
111
111
  '''Test for calling a tool as --tool=<tool>=</path/to/tool-exe>'''
112
112
  verilator_fullpath = shutil.which('verilator')
@@ -659,6 +659,31 @@ class TestsRequiresIVerilog(Helpers):
659
659
  assert rc == 0
660
660
 
661
661
 
662
+ @pytest.mark.skipif(not can_run_eda_sim(), reason='no tool found to handle command: sim')
663
+ class TestArgs(Helpers):
664
+ '''Test some args features, needs a sim tool'''
665
+ DEFAULT_DIR = os.path.join(THISPATH, '..', '..', 'lib', 'tests')
666
+
667
+ def test_duplicate_args(self):
668
+ '''Use oclib_fifo_test to make sure we don't lose (do NOT uniquify) duplicate
669
+ list-style args'''
670
+ self.chdir()
671
+ rc = self.log_it(
672
+ 'sim --stop-before-compile oclib_fifo_test --compile-args=-hi --compile-args=-hi',
673
+ use_eda_wrap=False
674
+ )
675
+ assert rc == 0
676
+ # Confirm we have two args in self.args['compile-args'] for: -hi
677
+ eda_config_yml_path = os.path.join(
678
+ os.getcwd(), 'eda.work', 'oclib_fifo_test.sim', 'eda_output_config.yml'
679
+ )
680
+ data = yaml_safe_load(eda_config_yml_path)
681
+ assert 'args' in data
682
+ assert 'compile-args' in data['args']
683
+ assert len(data['args']['compile-args']) == 2
684
+ assert data['args']['compile-args'] == ['-hi', '-hi']
685
+
686
+
662
687
  @pytest.mark.skipif(not can_run_eda_sim(), reason='no tool found to handle command: sim')
663
688
  class TestDepsReqs:
664
689
  '''Tests for 'reqs' in the DEPS files. 'reqs' are requirements, like a .pcap or file
@@ -708,65 +733,6 @@ class TestDepsReqs:
708
733
  assert rc > 1
709
734
 
710
735
 
711
- @pytest.mark.parametrize("command", ['sim', 'shell'])
712
- def test_deps_command_order(command):
713
- '''Test for various "commands" within a DEPS target. This test checks that command
714
- order is preserved in the top-to-bottom deps order, meaning that eda.py has to collect
715
- all commands deps order, and then execute them in that order.'''
716
-
717
- chdir_remove_work_dir('deps_files/command_order')
718
- if command == 'sim' and not can_run_eda_sim():
719
- pytest.skip(f'sim skipped, {can_run_eda_sim()=}')
720
- return # skip/pass
721
-
722
- if command == 'shell':
723
- cmd_list = 'shell target_test'.split()
724
- else:
725
- cmd_list = 'sim --stop-before-compile target_test'.split()
726
-
727
- with open('eda.log', 'w', encoding='utf-8') as f:
728
- with redirect_stdout(f):
729
- with redirect_stderr(f):
730
- rc = eda.main(*cmd_list)
731
-
732
- print(f'{rc=}')
733
- assert rc == 0
734
-
735
- # We should see "hi" before "bye" to confirm deps + command order is correct.
736
- # see ./deps_files/command_order/DEPS.yml - target = target_test
737
- found_str_list = [
738
- 'exec: echo "hi"',
739
- 'exec: echo "bye"',
740
- ]
741
- found_lines_list = [None, None]
742
-
743
- with open('eda.log', encoding='utf-8') as f:
744
- for lineno, line in enumerate(f.readlines()):
745
- line = line.rstrip()
746
- for idx,key in enumerate(found_str_list):
747
- if key in line:
748
- found_lines_list[idx] = lineno
749
-
750
- assert found_lines_list[0] # found hi
751
- assert found_lines_list[1] # found bye
752
- assert found_lines_list[0] < found_lines_list[1] # hi before bye
753
-
754
- # Added check, we redirected to create eda.log earlier to confirm the targets worked,
755
- # but as a general eda.py check, all shell commands should create their own
756
- # {target}__shell_0.log file:
757
- work_dir = os.path.join(
758
- THISPATH, 'deps_files', 'command_order', 'eda.work', f'target_test.{command}'
759
- )
760
- # Note that eda will write out the returncode INFO line to tee'd log files, so
761
- # there is more in the log file than "hi" or "bye".
762
- with open(os.path.join(work_dir, 'target_echo_hi__shell_0.log'), encoding='utf-8') as f:
763
- text = ' '.join(f.readlines()).strip()
764
- assert any(text.startswith(x) for x in ['hi', '"hi"', '\\"hi\\"'])
765
- # Added check, one of the targets uses a custom 'tee' file name, instead of the default log.
766
- with open(os.path.join(work_dir, 'custom_tee_echo_bye.log'), encoding='utf-8') as f:
767
- text = ''.join(f.readlines()).strip()
768
- assert any(text.startswith(x) for x in ['bye', '"bye"', '\\"bye\\"'])
769
-
770
736
 
771
737
  @pytest.mark.skipif('verilator' not in tools_loaded, reason="requires verilator")
772
738
  class TestDepsOtherMarkup: