FIX: Unhide cursor in data/python/FreeFOAM/tutorial.py
[freefoam.git] / data / python / FreeFOAM / tutorial.py
blob77fc67431302bdad373b2bb4f090753b53f1f4de
1 #-------------------------------------------------------------------------------
2 # ______ _ ____ __ __
3 # | ____| _| |_ / __ \ /\ | \/ |
4 # | |__ _ __ ___ ___ / \| | | | / \ | \ / |
5 # | __| '__/ _ \/ _ ( (| |) ) | | |/ /\ \ | |\/| |
6 # | | | | | __/ __/\_ _/| |__| / ____ \| | | |
7 # |_| |_| \___|\___| |_| \____/_/ \_\_| |_|
9 # FreeFOAM: The Cross-Platform CFD Toolkit
11 # Copyright (C) 2008-2011 Michael Wild <themiwi@users.sf.net>
12 # Gerber van der Graaf <gerber_graaf@users.sf.net>
13 #-------------------------------------------------------------------------------
14 # License
15 # This file is part of FreeFOAM.
17 # FreeFOAM is free software; you can redistribute it and/or modify it
18 # under the terms of the GNU General Public License as published by the
19 # Free Software Foundation; either version 2 of the License, or (at your
20 # option) any later version.
22 # FreeFOAM is distributed in the hope that it will be useful, but WITHOUT
23 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
24 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
25 # for more details.
27 # You should have received a copy of the GNU General Public License
28 # along with FreeFOAM; if not, write to the Free Software Foundation,
29 # Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
31 # Description
32 # Useful Python functionality to run FreeFOAM tutorial cases
34 #-------------------------------------------------------------------------------
36 """Classes and functions useful for running FreeFOAM tutorial applications."""
38 # want to be future proof
39 from FreeFOAM.compat import *
41 import os as _os
42 import os.path as _op
43 import FreeFOAM as _f
44 import FreeFOAM.run as _fr
45 import FreeFOAM.util as _fu
47 compile_config = _f.BUILD_TYPE
49 class ConfiguringFailedError(Exception):
50 """Thrown if configuring a tutorial application failed."""
51 def __init__(self, bindir, logfile):
52 """Initialize the exception object.
54 Parameters
55 ----------
56 bindir : The binary directory.
57 logfile : Name of the logfile which marks the case as run.
59 """
60 Exception.__init__(self, bindir, logfile)
62 def __str__(self):
63 """Return string representation of the error."""
64 return ('*** ERROR *** Configuring application %s failed.\n' +
65 'Refer to the logfile %s for more information.')%self.args
67 class CompilingFailedError(Exception):
68 """Thrown if the compilation of a tutorial application failed."""
69 def __init__(self, bindir, logfile):
70 """Initialize the exception object.
72 Parameters
73 ----------
74 bindir : The binary directory where the compilation should have run.
75 logfile : Name of the logfile which marks the case as run.
77 """
78 Exception.__init__(self, bindir, logfile)
80 def __str__(self):
81 """Return string representation of the error."""
82 return ('*** ERROR *** Cleaning application %s failed.\n' +
83 'Refer to the logfile %s for more information.')%self.args
85 class CleaningFailedError(Exception):
86 """Thrown if the cleaning of a tutorial application failed."""
87 def __init__(self, bindir):
88 """Initialize the exception object.
90 Parameters
91 ----------
92 app : The application that was already run.
93 logfile : Name of the logfile which marks the case as run.
95 """
96 Exception.__init__(self, bindir)
98 def __str__(self):
99 """Return string representation of the error."""
100 return ('*** ERROR *** Cleaning application %s failed.'%self.args)
102 class CaseRunner(object):
103 """A case runner.
105 Attributes
106 ----------
107 name : Name of the case.
108 case_dir : Directory containing the case.
109 test_dir : Directory to use in test runs.
110 default_schemes : Use the default finite volume schemes in test runs.
111 Defaults to False.
114 def __init__(self, name, case_dir='.', test_dir=None, skip_test=False):
115 """Initializes a new CaseRunner instance.
117 Parameters
118 ----------
119 name : Name of the case.
120 case_dir : Directory containing the case. Defaults to
121 `os.getcwd()`.
122 test_dir : Directory to create when cloning the case for the test
123 run. Defaults to `case_dir-test`.
124 skip_test : Default value to use when adding a new step.
127 self.name = name
128 self.case_dir = _op.abspath(case_dir)
129 if test_dir == None:
130 test_dir = self.case_dir+'-test'
131 self.test_dir = _op.abspath(test_dir)
132 self.skip_test = skip_test
133 self.default_schemes = False
134 self._stepnames = []
135 self._steps = {}
137 def name(self):
138 """Returns the name of this tutorial."""
139 return self._name
141 def steps(self):
142 """Retrieves the name of all registered steps."""
143 return self._stepnames
145 def add_step(self, stepname, cmd, stamp_file=None, skip_test=None):
146 """Registers a callable object to be run as a tutorial step.
148 Parameters
149 ----------
150 stepname : Name of the step.
151 cmd : Callable (function or object with __call__ function).
152 stamp_file : File to use as a stamp. If absolute, relative path to
153 `self.case_dir` will be used, and then appended to either
154 `self.case_dir` or `self.test_dir`. Defaults to
155 `'log.'+stepname`.
156 skip_test : Skip this step in test mode. Defaults to `self.skip_test`.
158 Note
159 ----
160 `cmd` must have the following signature:
161 def command(case_dir, stamp_file, test_mode):
162 # execute step
163 # write stamp file
164 return result
165 where `case_dir` is the the absolute path to where the case is contained
166 in. `stamp_file` is a `file` object the command should write its log to.
167 The last line must either read `REPORT: SUCCESS` or `REPORT: FAILURE` (as
168 e.g. created by `RunApp.__call__()`). Upon success, the function returns
169 True, False otherwise.
172 assert stepname not in self._stepnames, (
173 "Step "+stepname+" already exists")
174 if stamp_file == None:
175 stamp_file = 'log.'+stepname
176 if _op.isabs(stamp_file):
177 stamp_file = _op.relpath(stamp_file, self.case_dir)
178 if skip_test == None:
179 skip_test = self.skip_test
180 self._stepnames.append(stepname)
181 self._steps[stepname] = {
182 'cmd' : cmd,
183 'stamp' : stamp_file,
184 'skip_test' : skip_test,
187 def add_app_step(self, stepname, app=None, parallel=False, args=[],
188 stamp_file=None, skip_test=None):
189 """Convenience function to add a step running a FreeFOAM application.
191 Parameters
192 ----------
193 stepname : Name of the step.
194 app : Name of the application to run. Defaults to `stepname`.
195 parallel : If True, run the application in parallel.
196 args : Additional arguments to be passed to the application.
197 stamp_file : File to use as a stamp. Relative to `self.case_dir`.
198 Defaults to `'log.'+app`.
199 skip_test : Skip this step in test mode.
202 if app == None:
203 app = stepname
204 if stamp_file == None:
205 stamp_file = 'log.'+_op.basename(stepname)
206 self.add_step(stepname, RunApp(app, parallel, args),
207 stamp_file=stamp_file, skip_test=skip_test)
209 def run(self, step=None, test_mode=False, verbose=False):
210 """Run the case as a whole or a specific step.
212 A step is only run if at least one of the following applies:
213 * `test_mode` is True.
214 * The stamp file of the test (usually the log file) does not exist.
215 * The stamp file of the preceeding step is newer than the one for this
216 step.
218 Every step is assumed to depend on the preceeding step, and above
219 criteria propagate up to the first step if required (except for
220 `test_mode` which only applies to the specified step, or to all steps if
221 `step` is None). This means that if you specify a step to be run and any
222 of the preceeding steps has not been run or is out of date, that step and
223 all the ones in between will also be run.
225 Parameters
226 ----------
227 step : Name of the step to be run. If `None`, try to run all steps.
228 test_mode : Run in testing mode.
229 verbose : Echo the output to standard output.
231 Returns
232 -------
233 True if the steps all ran successfully, False otherwise.
236 import sys
237 assert len(self._stepnames) > 0, "No steps registered"
239 if step == None:
240 step = self._stepnames[-1]
241 assert step in self._stepnames, "Step "+step+" not registered"
243 if test_mode:
244 self.clone_test()
245 case_dir = self.test_dir
246 else:
247 case_dir = self.case_dir
248 case_dir = _op.abspath(case_dir)
250 echo('Running case '+case_dir)
252 stat = True
253 fmt = ' %%-%ds ${HIDE_CURSOR}'%max(
254 30, max(map(len, self._stepnames)))
255 last = self._stepnames.index(step)
256 try:
257 for i, s in enumerate(self._stepnames):
258 if not verbose:
259 _fu.cecho(fmt%s, end='')
260 sys.stdout.flush()
261 stamp = _op.join(case_dir, self._steps[s]['stamp'])
262 do_skip = False
263 if test_mode and self._steps[s]['skip_test']:
264 do_skip = True
265 else:
266 if i > last:
267 do_skip = True
268 elif _op.isfile(stamp):
269 do_skip = True
270 if i > 0:
271 old_s = self._stepnames[i-1]
272 old_stamp = _op.join(
273 case_dir, self._steps[old_s]['stamp'])
274 if _op.getmtime(old_stamp) > _op.getmtime(stamp):
275 do_skip = False
276 if do_skip:
277 if not verbose:
278 _fu.cecho('${YELLOW}[ SKIP ]${NORMAL}')
279 continue
281 try:
282 if verbose:
283 stamp_file = _fu.Tee(stamp, 'wt')
284 else:
285 stamp_file = open(stamp, 'wt')
286 stat = self._steps[s]['cmd'](case_dir, stamp_file, test_mode)
287 if not verbose:
288 if stat:
289 _fu.cecho('${GREEN}[ OK ]${NORMAL}')
290 else:
291 _fu.cecho('${RED}${BOLD}[FAILED]${NORMAL}')
292 except BaseException, e:
293 if not verbose:
294 _fu.cecho('${RED}${BOLD}[FAILED]${NORMAL}')
295 _fu.cerror("Step %s in case %s raised an exception:\n%s"%
296 (s, case_dir, str(e)), file=sys.stderr)
297 stat = False
298 raise
299 if not stat:
300 break
301 finally:
302 _fu.cecho('${SHOW_CURSOR}', end='')
303 return stat
305 def clean(self):
306 """Cleans the case.
308 This function is supposed to be overridden by tutorials requiring more
309 cleaning up than `FreeFOAM.util.clean_case` provides.
311 Note that this implementation also removes `test_dir` if it exist
312 and is not equal to the `case_dir`.
315 _fu.clean_case(self.case_dir)
316 if self.test_dir != self.case_dir and _op.isdir(self.test_dir):
317 _fu.remove_case(self.test_dir)
319 def clone_test(self):
320 """Creates a testing clone.
322 This function is supposed to be overridden by tutorials requiring more
323 cloning work than `FreeFOAM.util.clone()` and `modify_for_test()`
324 provide.
327 if _op.isdir(self.test_dir):
328 _fu.remove_case(self.test_dir)
329 _fu.clone_case(self.case_dir, self.test_dir)
330 self.modify_for_test()
332 def modify_for_test(self):
333 """Modifies the case `test_dir` for use as a test case."""
334 self.modify_global_controlDict_for_test(self.test_dir)
335 self.modify_case_for_test(self.test_dir)
337 def modify_global_controlDict_for_test(self, test_dir):
338 """Copies the global etc directory and modifies if for testing."""
339 ctrld = _op.join(test_dir, 'etc', 'controlDict')
340 if not _op.isdir(_op.dirname(ctrld)):
341 _os.makedirs(_op.dirname(ctrld))
342 _fu.copytree(_op.join(_f.CONFIG_DIR, 'controlDict'), ctrld)
343 _fu.copytree(
344 _op.join(_f.CONFIG_DIR, 'cellModels'),
345 _op.join(test_dir, 'etc', 'cellModels'))
346 _fu.copytree(
347 _op.join(_f.CONFIG_DIR, 'thermoData'),
348 _op.join(test_dir, 'etc', 'thermoData'))
349 modify_file(ctrld, [
350 (r'(fvSchemes\s*)([0-9]);', r'\g<1>1;'),
351 (r'(solution\s*)([0-9]);', r'\g<1>1;') ])
353 def modify_case_for_test(self, test_dir):
354 "Modifies system/{controlDict,fvSchemes,snappyHexMeshDict} for testing."
355 import re
356 # modify system/controlDict
357 for e in ['', '.1st']:
358 ctrld = _op.join(test_dir, 'system', 'controlDict'+e)
359 if _op.isfile(ctrld):
360 modify_file(ctrld, [
361 (r'(startFrom\s+)([a-zA-Z]+);', r'\g<1>latestTime;'),
362 (r'(stopAt\s+)([a-zA-Z]*);', r'\g<1>nextWrite;'),
363 (r'(writeControl\s+)([a-zA-Z]+);', r'\g<1>timeStep;'),
364 (r'(writeInterval\s+)([0-9a-zA-Z.-]+);', r'\g<1>1;'),
365 ], '.orig')
366 # modify system/fvSchemes
367 if self.default_schemes:
368 fv_schemes = (
369 'gradScheme',
370 'divScheme',
371 'laplacianScheme',
372 'interpolationScheme',
373 'snGradScheme',
374 'fluxRequired',
376 for parent, dirs, files in _os.walk(_op.join(test_dir,'system')):
377 if 'fvSchemes' in files:
378 fvs = _op.join(parent, 'fvSchemes')
379 lines = open(fvs, 'rt').readlines()
380 kill_lines = []
381 n_brace = 0
382 search_start = False
383 search_end = False
384 for i, l in enumerate(lines):
385 m = re.search('|'.join(fv_schemes), l)
386 if n_brace > 0 or search_start or m:
387 kill_lines.append(i)
388 if m:
389 n_brace = 0
390 search_start = True
391 search_end = False
392 if search_start or search_end:
393 n_brace += l.count('{')
394 if n_brace > 0:
395 search_start = False
396 search_end = True
397 n_brace -= l.count('}')
398 if search_end and n_brace < 1:
399 search_end = False
400 for i in reversed(kill_lines):
401 del lines[i]
402 f = open(fvs, 'wt')
403 f.writelines(lines)
404 f.write("""
405 gradSchemes { default Gauss linear; }
406 divSchemes
408 default Gauss linear;
409 div(phi,fu_ft_h) Gauss multivariateSelection
411 fu upwind;
412 ft upwind;
413 h upwind;
415 div(phi,ft_b_h_hu) Gauss multivariateSelection
417 fu upwind;
418 ft upwind;
419 bprog upwind;
420 h upwind;
421 hu upwind;
424 laplacianSchemes { default Gauss linear corrected; }
425 interpolationSchemes { default linear; }
426 snGradSchemes { default corrected; }
427 fluxRequired { default yes; }
428 """ )
429 f.close()
431 class ClonedCaseRunner(CaseRunner):
432 """A runner for cases that need to be cloned from another case."""
433 def __init__(self, name, parent_runner, case_dir='.',
434 test_dir=None, skip_test=False):
435 CaseRunner.__init__(self, name, case_dir=case_dir,
436 test_dir=test_dir, skip_test=skip_test)
437 self._parent_runner = parent_runner
439 def clone_from_parent(self, case_dir):
440 """Clones the case from its parent case."""
441 try:
442 parent_dir = self._parent_runner.case_dir
443 if not _op.isdir(_op.join(case_dir, 'system')):
444 _fu.clone_case(parent_dir, case_dir, verbose=False)
445 return True
446 except Exception, e:
447 import sys
448 _fu.cerror("Failed to clone", parent_dir, ":", str(e),
449 file=sys.stderr)
450 return False
452 def run(self, step=None, test_mode=False, verbose=False):
453 """Refer to `CaseRunner.run`."""
454 if not test_mode:
455 self.clone_from_parent(self.case_dir)
456 return CaseRunner.run(self, step=step, test_mode=test_mode,
457 verbose=verbose)
459 def clean(self):
460 """Removes the cloned case and, if it exists, the test-case."""
461 _fu.remove_case(self.case_dir)
462 if _op.isdir(self.test_dir):
463 _fu.remove_case(self.test_dir)
465 def clone_test(self):
466 """Refer to `CaseRunner.clean`."""
467 if _op.isdir(self.test_dir):
468 _fu.remove_case(self.test_dir)
469 self.clone_from_parent(self.test_dir)
470 self.modify_for_test()
472 class TutorialRunner(object):
473 """A tutorial case runner.
475 Attributes
476 ----------
477 default_schemes : Use the default finite volume schemes in test runs.
478 Defaults to False.
479 keep_going : Do not abort after a failed case. Defaults to False.
480 test_mode : Run in testing mode. Defaults to False.
481 verbose : Echo the output to standard output. Defaults to False.
484 def __init__(self, default_schemes=False, keep_going=False, test_mode=False,
485 verbose=False):
486 """Initializes a new TutorialRunner instance."""
487 self.default_schemes = default_schemes
488 self.keep_going = keep_going
489 self.test_mode = test_mode
490 self.verbose = verbose
491 self.did_run = False
492 self._cases = []
494 def cases(self):
495 """Returns a tuple of all registered case names."""
496 return tuple([c.name for c in self._cases])
498 def steps(self):
499 """Returns a tuple of all registered step names."""
500 steps = []
501 for c in self._cases:
502 for s in c.steps():
503 steps.append('.'.join((c.name,s)))
504 return steps
506 def add_case(self, case):
507 """Adds a case."""
508 assert case.name not in self.cases(), (
509 "Trying to re-register case "+case.name)
510 self._cases.append(case)
512 def run(self, step=None):
513 """Run the case as a whole or a specific step.
515 A step is only run if at least one of the following applies:
516 * `self.test_mode` is True.
517 * The stamp file of the test (usually the log file) does not exist.
518 * The stamp file of the preceeding step of the same case is newer than
519 the one for this step.
521 Every step is assumed to depend on the preceeding step of the same case,
522 and above criteria propagate up to the first step if required (except for
523 `self.test_mode` which only applies to the specified step, or to all
524 steps if `step` is None). This means that if you specify a step to be run
525 and any of the preceeding steps of the same case has not been run or is
526 out of date, that step and all the ones in between will also be run.
528 In test mode all steps belonging to the same case are run (up to the
529 specified one), except in the default case, where all steps in all cases
530 are run.
532 The names of the steps are composed of the case name and the step name,
533 separated by a dot. Only specifying the case name is equivalent to
534 running all the steps belonging to that case.
536 Parameters
537 ----------
538 step : Name of the step to be run. If `None`, try to run all steps.
540 Returns
541 -------
542 True if the steps all ran successfully, False otherwise.
545 case = None
546 substep = None
547 if step != None:
548 case_and_step = step.split('.', 1)
549 case = case_and_step[0]
550 if len(case_and_step) > 1:
551 substep = case_and_step[1]
552 cases = self.cases()
553 if case:
554 cases = [case]
555 stat = False
556 for c in filter(lambda c: c.name in cases, self._cases):
557 c.default_schemes = self.default_schemes
558 stat = False
559 try:
560 stat = c.run(substep, self.test_mode, self.verbose)
561 except:
562 if not self.keep_going:
563 raise
564 if not stat and not self.keep_going:
565 break
566 return stat or self.keep_going
568 def clean(self):
569 """Clean all cases."""
570 for c in self._cases:
571 c.clean()
573 def main(self):
574 """Run as the main function in a script.
575 <step> Optionally, a step can be specifiedo which should be
576 run. By default all steps are run. Refer to run() for
577 the details.
580 import optparse
581 import sys
582 parser = optparse.OptionParser(usage='Usage: %prog [options] [step]')
583 parser.add_option('-t', '--test', dest='testmode', action='store_true',
584 default=False, help=('Run in testing mode. This causes the case ' +
585 'to be cloned to a test directory.'))
586 parser.add_option('-k', '--keep-going', dest='keep_going',
587 action='store_true', default=False,
588 help='Don\'t abort after a failed case.')
589 parser.add_option('-d', '--default-schemes', dest='default_schemes',
590 action='store_true', default=False,
591 help='Run using a set of default fvSchemes.')
592 parser.add_option('-l', '--list', dest='list', action='store_true',
593 default=False, help='List the individual steps in this tutorial.')
594 parser.add_option('-c', '--cases', dest='cases', action='store_true',
595 default=False, help='Print the cases in this tutorial.')
596 parser.add_option('--clean', dest='clean', action='store_true',
597 default=False, help='Clean this tutorial case.')
598 parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
599 default=False, help=('Echo the output to standard output'))
601 (opts, args) = parser.parse_args()
602 if opts.list and (opts.testmode or opts.default_schemes or
603 opts.keep_going or opts.cases or opts.clean or opts.verbose or
604 len(args)>0):
605 parser.error(
606 'Option --list may not be used with arguments and other options')
607 if opts.cases and (opts.testmode or opts.default_schemes or
608 opts.keep_going or opts.list or opts.clean or opts.verbose or
609 len(args)>0):
610 parser.error(
611 'Option --cases may not be used with arguments and other options')
612 if opts.clean and (opts.testmode or opts.default_schemes or
613 opts.keep_going or opts.list or opts.cases or opts.verbose or
614 len(args)>0):
615 parser.error(
616 'Option --clean may not be used with arguments and other options')
617 if opts.default_schemes and not opts.testmode:
618 parser.error(
619 'Option --default-schemes can only be used with --test')
620 if len(args) > 1:
621 parser.error(
622 'May not be called with more than one argument')
624 if opts.list:
625 echo('\n'.join(self.steps()))
626 return 0
627 if opts.cases:
628 echo('\n'.join(self.cases()))
629 return 0
630 if opts.clean:
631 self.clean()
632 return 0
634 self.default_schemes = opts.default_schemes
635 self.keep_going = opts.keep_going
636 self.test_mode = opts.testmode
637 self.verbose = opts.verbose
638 self.did_run = True
639 step = None
640 if len(args):
641 step = args[0]
642 return int(not self.run(step))
644 class RunApp(object):
645 """Functor to run a FreeFOAM application."""
646 def __init__(self, app, parallel=False, args=[]):
647 """Creates a new application runner.
649 Parameters
650 ----------
651 app : Name or full/relative path to the application to run.
652 parallel : If True, the '-parallel' flag will be passed to the
653 application.
654 args : Additional arguments and options to be passed to the
655 application.
658 self._app = app
659 self._parallel = parallel
660 self._args = args
661 self._runner = _fr.ParallelRunner()
663 def __call__(self, case_dir, stamp_file, testmode):
664 """Runs the FreeFOAM application.
666 The function redirects the output to `self.logfile`. Before running
667 the application, it writes a short header to the log, listing the
668 application name, the case directory and the command line used to
669 invoke the application. At the end of the run it writes a `REPORT:
670 SUCCESS` if the application returned 0, or `REPORT: FAILURE` for all
671 other return values or if an exception was caught.
673 Returns
674 -------
675 True on success, False otherwise.
678 import sys
679 import subprocess
680 returncode = 1
681 stamp_file.write(''.join([80*'='+'\nAPPLICATION: ', self._app, '\n']))
682 stamp_file.write(''.join(['CASE: ', case_dir, '\n']))
683 stamp_file.write(''.join(['COMMAND LINE: ', self._runner.command_str(
684 self._app, case=case_dir, parallel=self._parallel,
685 args=self._args), '\n'+80*'='+'\n\n']))
686 stamp_file.flush()
687 try:
688 # this is ridiculous, but python 2.4 doesn't support try-except-finally
689 try:
690 if testmode:
691 _os.environ['FREEFOAM_CONFIG_DIR'] = _op.join(case_dir, 'etc')
692 self._runner.run(self._app, case=case_dir,
693 parallel=self._parallel, stdout=subprocess.PIPE,
694 stderr=subprocess.STDOUT, args=self._args, background=True) == 0
695 # retrieve the output and write it to the log file as long as the thing runs
696 while True:
697 lines = self._runner.getProcess().stdout.readlines()
698 if len(lines) and not isinstance(lines[0], str):
699 lines = list(map(lambda l: l.decode(), lines))
700 stamp_file.writelines(lines)
701 stat = self._runner.poll()
702 if stat != None:
703 break
704 stat = stat == 0
705 except KeyboardInterrupt:
706 stamp_file.write('\n*** Error *** KeyboardInterrupt\n')
707 stat = False
708 raise
709 except Exception, e:
710 stamp_file.write('\n*** Error *** '+str(e)+'\n')
711 stat = False
712 raise
713 finally:
714 # write final verdict to the log and clean up
715 stamp_file.write('\n'+80*'='+'\nREPORT: ')
716 if stat:
717 stamp_file.write('SUCCESS\n')
718 else:
719 stamp_file.write('FAILURE\n')
720 return stat
722 class CompileApp(object):
723 """Functor to compile a FreeFOAM tutorial application.
725 Attributes
726 ----------
727 srcdir : The top-level source directory.
728 bindir : The top-level binary directory.
729 app : Absolute path to the binary application.
732 def __init__(self, srcdir, bindir=None, name=None):
733 """Initializes a new CompileApp instance.
735 Parameters
736 ----------
737 srcdir : The top-level source directory.
738 bindir : The top-level binary directory (i.e. where to build the
739 application). If set to None, defaults to 'srcdir+"-build"'.
740 name : Name of the application. Defaults to `os.path.basename(srcdir)`.
743 self.srcdir = _op.abspath(srcdir)
744 if bindir == None:
745 bindir = self.srcdir+'-build'
746 if name == None:
747 name = _op.basename(self.srcdir)
748 self.bindir = bindir
749 self.app = _op.join(self.bindir, _f.EXE_PREFIX+name+_f.EXE_SUFFIX)
751 def __call__(self, case_dir, stamp_file, testmode):
752 """Compile a FreeFOAM application.
754 Configures and compiles a FreeFOAM tutorial application located in the
755 source directory `self.srcdir`.
758 import subprocess
759 # get the current working directory
760 pwd = _os.getcwd()
761 # create the binary directory and cd to it
762 if not _op.exists(self.bindir):
763 _os.makedirs(self.bindir)
764 elif not _op.isdir(self.bindir):
765 raise BinDirCreationError(self.bindir,
766 'The path exists, but is not a directory')
767 returncode = 1
768 msg = None
769 _os.chdir(self.bindir)
770 # configure the project
771 cmd = [
772 _f.CMAKE_COMMAND,
773 '-DCMAKE_C_COMPILER='+_f.CMAKE_C_COMPILER,
774 '-DCMAKE_CXX_COMPILER='+_f.CMAKE_CXX_COMPILER,
775 '-DCMAKE_BUILD_TYPE='+compile_config,
776 '-G', _f.CMAKE_GENERATOR,
777 self.srcdir
779 stamp_file.write(''.join(['CONFIGURING: ', ' '.join(cmd),
780 '\n'+80*'='+'\n\n']))
781 stamp_file.flush()
782 try:
783 process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
784 stderr=subprocess.STDOUT)
785 while True:
786 lines = process.stdout.readlines()
787 if len(lines) and not isinstance(lines[0], str):
788 lines = list(map(lambda l: l.decode(), lines))
789 stamp_file.writelines(lines)
790 returncode = process.poll()
791 if returncode != None:
792 break
793 stamp_file.write('\n'+80*'='+'\nREPORT: ')
794 if returncode:
795 msg = 'Failed to configure the project "%s" in "%s".'%(
796 self.srcdir, self.bindir)
797 stamp_file.write('FAILURE\n')
798 return False
799 else:
800 stamp_file.write('SUCCESS\n')
801 # kick off the build
802 cmd = [_f.CMAKE_COMMAND, '--build', '.',
803 '--config', compile_config]
804 stamp_file.write(''.join(['COMPILING: ', ' '.join(cmd),
805 '\n'+80*'='+'\n\n']))
806 stamp_file.flush()
807 returncode = subprocess.call(
808 cmd, stdout=stamp_file, stderr=stamp_file)
809 stamp_file.write('\n'+80*'='+'\nREPORT: ')
810 if returncode:
811 msg = 'Failed to build the application "%s" in "%s".'%(
812 self.srcdir, self.bindir)
813 stamp_file.write('FAILURE\n')
814 return False
815 else:
816 stamp_file.write('SUCCESS\n')
817 finally:
818 _os.chdir(pwd)
819 return True
822 def clean_application(bindir):
823 """Cleans a FreeFOAM application.
825 Cleans a FreeFOAM tutorial application located in the binary directory
826 `bindir`.
828 Parameters
829 ----------
830 bindir : The top-level binary directory (i.e. where the build directory is.
833 import subprocess
834 bindir = _op.normpath(bindir)
835 returncode = 0
836 if _op.isdir(bindir):
837 cmd = [_f.CMAKE_COMMAND, '--build', bindir, '--target', 'clean']
838 returncode = subprocess.call(cmd)
839 if returncode:
840 raise CleaningFailedError(bindir)
841 return
843 def modify_file(fname, regSub, backup=None):
844 """Modifies file `fname` by applying the regular expression/substitution
845 pairs in the iterable `regSub`. If `backup` is a string and not None, a
846 backup file with the given suffix will be created before the modification
847 takes place."""
848 import re
849 fname = _op.abspath(fname)
850 if backup and type(backup) == str:
851 import shutil
852 shutil.copy2(fname, fname+backup)
853 lines=open(fname, 'rt').readlines()
854 for i, l in enumerate(lines):
855 for p, s in regSub:
856 if re.search(p, l):
857 lines[i] = re.sub(p, s, l)
858 open(fname, 'wt').writelines(lines)
860 # ------------------------- vim: set sw=3 sts=3 et: --------------- end-of-file