diff options
-rw-r--r-- | README.md | 282 | ||||
-rwxr-xr-x | run_exps.py | 11 |
2 files changed, 150 insertions, 143 deletions
@@ -1,118 +1,97 @@ | |||
1 | I. INTRODUCTION | 1 | # About |
2 | These scripts provide a common way for creating, running, parsing, and | 2 | These Python scripts provide a common way for creating, running, parsing, and plotting experiments using [LITMUS^RT][litmus]. These scripts are: |
3 | plotting experiments under LITMUS^RT. They are designed with the | ||
4 | following principles in mind: | ||
5 | |||
6 | 1. Little or no configuration: all scripts use certain parameters to | ||
7 | configure behavior. However, if the user does not give these | ||
8 | parameters, the scripts will examine the properties of the user's | ||
9 | system to pick a suitable default. Requiring user input is a last | ||
10 | resort. | ||
11 | |||
12 | 2. Interruptability: the scripts save their work as they evaluate | ||
13 | multiple directories. When the scripts are interrupted, or if new data | ||
14 | is added to those directories, the scripts can be re-run and they will | ||
15 | resume where they left off. This vastly decreases turnaround time for | ||
16 | testing new features. | ||
17 | |||
18 | 3. Maximum Safety: where possible, scripts save metadata in their output | ||
19 | directories about the data contained. This metadata can be used by | ||
20 | the other scripts to safely use the data later. | ||
21 | |||
22 | 4. Independence / legacy support: none of these scripts assume their | ||
23 | input was generated by another of these scripts. Three are designed to | ||
24 | recognize generic input formats inspired by past LITMUS^RT | ||
25 | experimental setups. (The exception to this is gen_exps.py, which | ||
26 | has only user intput and creates output only for run_exps.py) | ||
27 | |||
28 | 5. Save everything: all output and parameters (even from subprocesses) | ||
29 | is saved for debugging / reproducability. This data is saved in tmp/ | ||
30 | directories while scripts are running in case scripts fail. | ||
31 | |||
32 | These scripts require that the following repos are in the user's PATH: | ||
33 | 1. liblitmus - for real-time executable simulation and task set release | ||
34 | 2. feather-trace-tools - for recording and parsing overheads and | ||
35 | scheduling events | ||
36 | |||
37 | Optionally, additional features will be enabled if these repos are | ||
38 | present in the PATH: | ||
39 | 1. rt-kernelshark - to record ftrace events for kernelshark visualization | ||
40 | 2. sched_trace - to output a file containing scheduling events as | ||
41 | strings | ||
42 | |||
43 | Each of these scripts is designed to operate independently of the | ||
44 | others. For example, the parse_exps.py will find any feather trace | ||
45 | files resembling ft-xyz.bin or xyz.ft and print out overhead | ||
46 | statistics for the records inside. However, the scripts provide the | ||
47 | most features (especially safety) when their results are chained | ||
48 | together, like so: | ||
49 | 3 | ||
4 | 1. `gen_exps.py`: for creating sets of experiments | ||
5 | 2. `run_exps.py`: for running and tracing experiments | ||
6 | 3. `parse_exps.py`: for parsing LITMUS^RT trace data | ||
7 | 4. `plot_exps.py`: for plotting directories of csv data | ||
8 | |||
9 | They are designed with the following principles in mind: | ||
10 | |||
11 | 1. Little or no configuration: all scripts use certain parameters to configure behavior. However, if the user does not give these parameters, the scripts will examine the properties of the user's system to pick a suitable default. Requiring user input is a last resort. | ||
12 | |||
13 | 2. Interruptability: the scripts save their work as they evaluate multiple directories. When the scripts are interrupted, or if new data is added to those directories, the scripts can be re-run and they will resume where they left off. This vastly decreases turnaround time for testing new features. | ||
14 | |||
15 | 3. Maximum Safety: where possible, scripts save metadata in their output directories about the data contained. This metadata can be used by the other scripts to safely use the data later. | ||
16 | |||
17 | 4. Independence / legacy support: none of these scripts assume their input was generated by another of these scripts. Three are designed to recognize generic input formats inspired by past LITMUS^RT experimental setups. (The exception to this is gen_exps.py, which has only user intput and creates output only for run_exps.py) | ||
18 | |||
19 | 5. Save everything: all output and parameters (even from subprocesses) is saved for debugging / reproducability. This data is saved in tmp/ directories while scripts are running in case scripts fail. | ||
20 | |||
21 | # Dependencies | ||
22 | These scripts were tested using Python 2.7.2. They have not been tested using Python 3. The [Matplotlib][matplotlib] Python library is needed for plotting. | ||
23 | |||
24 | The `run_exps.py` script should almost always be run using a LITMUS^RT kernel. In addition to the kernel, the following LITMUS-related repos must be in the user's `PATH`: | ||
25 | 1. [liblitmus][liblitmus]: for real-time executable simulation and task set release | ||
26 | 2. [feather-trace-tools][feather-trace-tools]: for recording and parsing overheads and scheduling events | ||
27 | |||
28 | Additional features will be enabled if these repos are present in the `PATH`: | ||
29 | 1. [rt-kernelshark][rt-kernelshark]: to record ftrace events for kernelshark visualization | ||
30 | 2. sched_trace ([UNC internal][rtunc]) to output a file containing scheduling events as strings | ||
31 | |||
32 | # Details | ||
33 | Each of these scripts is designed to operate independently of the others. For example, `parse_exps.py` will find any feather trace files resembling `ft-xyz.bin` or `xyz.ft` and print out overhead statistics for the records inside. However, the scripts provide the most features (especially safety) when their results are chained together, like so: | ||
34 | |||
35 | ``` | ||
50 | gen_exps.py --> [exps/*] --> run_exps.py --> [run-data/*] --. | 36 | gen_exps.py --> [exps/*] --> run_exps.py --> [run-data/*] --. |
51 | .------------------------------------------------------------' | 37 | .------------------------------------------------------------' |
52 | '--> parse_exps.py --> [parse-data/*] --> plot_exps.py --> [plot-data/*.pdf] | 38 | '--> parse_exps.py --> [parse-data/*] --> plot_exps.py --> [plot-data/*.pdf] |
39 | ``` | ||
40 | |||
41 | 1. Create experiments with `gen_exps.py` or some other script. | ||
42 | 2. Run experiments using `run_exps.py`, generating binary files in `run-data/`. | ||
43 | 3. Parse binary data in `run-data/` using `parse_exps.py`, generating csv files in `parse-data/`. | ||
44 | 4. Plot `parse-data` using `plot_exps.py`, generating pdfs in `plot-data/`. | ||
45 | |||
46 | Each of these scripts will be described. The `run_exps.py` script is first because `gen_exps.py` creates schedule files which depend on `run_exps.py`. | ||
47 | |||
53 | 48 | ||
54 | 0. Create experiments with gen_exps.py or some other script. | 49 | ## run_exps.py |
55 | 1. Run experiments using run_exps.py, generating binary files in run-data/. | 50 | *Usage*: `run_exps.py [OPTIONS] [SCHED_FILE]... [SCHED_DIR]...` |
56 | 2. Parse binary data in run-data using parse_exps.py, generating csv | 51 | |
57 | files in parse-data/. | 52 | where a `SCHED_DIR` resembles: |
58 | 3. Plot parse-data using plot_exps.py, generating pdfs in plot-data. | 53 | ``` |
59 | 54 | SCHED_DIR/ | |
60 | Each of these scripts will be described. The run_exps.py script is | 55 | SCHED_FILE |
61 | first because gen_exps.py creates schedule files which depend on run_exps.py. | 56 | PARAM_FILE |
62 | 57 | ``` | |
63 | 58 | ||
64 | II. RUN_EXPS | 59 | *Output*: `OUT_DIR/[files]` or `OUT_DIR/SCHED_DIR/[files]` or `OUT_DIR/SCHED_FILE/[files]` depending on input |
65 | Usage: run_exps.py [OPTIONS] [SCHED_FILE]... [SCHED_DIR]... | 60 | |
66 | where a SCHED_DIR resembles: | 61 | If all features are enabled, these files are: |
67 | SCHED_DIR/ | 62 | ``` |
68 | SCHED_FILE | 63 | OUT_DIR/[SCHED_(FILE|DIR)/] |
69 | PARAM_FILE | 64 | trace.slog # LITMUS logging |
70 | 65 | st-[1..m].bin # sched_trace data | |
71 | Output: OUT_DIR/[files] or OUT_DIR/SCHED_DIR/[files] or | 66 | ft.bin # feather-trace overhead data |
72 | OUT_DIR/SCHED_FILE/[files] depending on input | 67 | trace.dat # ftrace data for kernelshark |
73 | If all features are enabled, these files are: | 68 | params.py # Schedule parameters |
74 | OUT_DIR/[.*/] | 69 | exec-out.txt # Standard out from schedule processes |
75 | trace.slog # LITMUS logging | 70 | exec-err.txt # Standard err ''' |
76 | st-[1..m].bin # sched_trace data | 71 | ``` |
77 | ft.bin # feather-trace overhead data | 72 | |
78 | trace.dat # ftrace data for kernelshark | 73 | *Defaults*: `SCHED_FILE = sched.py, PARAM_FILE = params.py, DURATION = 30, OUT_DIR = run-data/` |
79 | params.py # Schedule parameters | 74 | |
80 | exec-out.txt # Standard out from schedule processes | 75 | This script reads *schedule files* (described below) and executes real-time task systems, recording all overhead, logging, and trace data which is enabled in the system. For example, if trace logging is enabled, rt-kernelshark is found in the path, but feather-trace is disabled (the devices are not present), only trace logs and rt-kernelshark logs will be recorded. |
81 | exec-err.txt # Standard err ''' | 76 | |
82 | 77 | When `run_exps.py` is running a schedule file, temporary data is saved in a `tmp` directory in the same directory as the schedule file. When execution completes, this data is moved into a directory under the `run_exps.py` output directory (default: `run-data/`, can be changed with the `-o` option). When multiple schedules are run, each schedule's data is saved in a unique directory under the output directory. | |
83 | Defaults: SCHED_FILE = sched.py, PARAM_FILE = params.py, | 78 | |
84 | DURATION = 30, OUT_DIR = run-data/ | 79 | If a schedule has been run and it's data is in the output directory, `run_exps.py` will not re-run the schedule unless the `-f` option is specified. This is useful if your system crashes midway through a set of experiments. |
85 | |||
86 | The run_exps.py script reads schedule files and executes real-time | ||
87 | task systems, recording all overhead, logging, and trace data which is | ||
88 | enabled in the system. For example, if trace logging is enabled, | ||
89 | rt-kernelshark is found in the path, but feather-trace is disabled | ||
90 | (the devices are not present), only trace-logs and kernelshark logs | ||
91 | will be recorded. | ||
92 | |||
93 | When run_exps.py is running a schedule file, temporary data is saved | ||
94 | in a 'tmp' directory in the same directory as the schedule file. When | ||
95 | execution completes, this data is moved into a directory under the | ||
96 | run_exps.py output directory (default: 'run-data/', can be changed with | ||
97 | the -o option). When multiple schedules are run, each schedule's data | ||
98 | is saved in a unique directory under the output directory. | ||
99 | |||
100 | If a schedule has been run and it's data is in the output directory, | ||
101 | run_exps.py will not re-run the schedule unless the -f option is | ||
102 | specified. This is useful if your system crashes midway through a set | ||
103 | of experiments. | ||
104 | 80 | ||
105 | Schedule files have one of the following two formats: | 81 | Schedule files have one of the following two formats: |
106 | 82 | ||
107 | a) simple format | 83 | 1. simple format |
84 | ``` | ||
108 | path/to/proc{proc_value} | 85 | path/to/proc{proc_value} |
109 | ... | 86 | ... |
110 | path/to/proc{proc_value} | 87 | path/to/proc{proc_value} |
111 | [real_time_task: default rtspin] task_arguments... | 88 | [real_time_task: default rtspin] task_arguments... |
112 | ... | 89 | ... |
113 | [real_time_task] task_arguments... | 90 | [real_time_task] task_arguments... |
91 | ``` | ||
114 | 92 | ||
115 | b) python format | 93 | b) python format |
94 | ```python | ||
116 | {'proc':[ | 95 | {'proc':[ |
117 | ('path/to/proc','proc_value'), | 96 | ('path/to/proc','proc_value'), |
118 | ..., | 97 | ..., |
@@ -124,55 +103,67 @@ b) python format | |||
124 | ('real_time_task', 'task_arguments') | 103 | ('real_time_task', 'task_arguments') |
125 | ] | 104 | ] |
126 | } | 105 | } |
106 | ``` | ||
127 | 107 | ||
128 | The following creates a simple 3-task system with utilization 2.0, | 108 | The following creates a simple 3-task system with utilization 2.0, which is then run under the `GSN-EDF` plugin: |
129 | which is then run under the GSN-EDF plugin: | ||
130 | 109 | ||
110 | ```bash | ||
131 | $ echo "10 20 | 111 | $ echo "10 20 |
132 | 30 40 | 112 | 30 40 |
133 | 60 90" > test.sched | 113 | 60 90" > test.sched |
134 | $ run_exps.py -s GSN-EDF test.sched | 114 | $ run_exps.py -s GSN-EDF test.sched |
135 | 115 | [Exp test/test.sched]: Enabling sched_trace | |
136 | The following will write a release master using | 116 | ... |
137 | /proc/litmus/release_master: | 117 | [Exp test/test.sched]: Switching to GSN-EDF |
138 | 118 | [Exp test/test.sched]: Starting 3 tracers | |
119 | [Exp test/test.sched]: Starting the programs | ||
120 | [Exp test/test.sched]: Sleeping until tasks are ready for release... | ||
121 | [Exp test/test.sched]: Releasing 3 tasks | ||
122 | [Exp test/test.sched]: Waiting for program to finish... | ||
123 | [Exp test/test.sched]: Saving results in /root/schedules/test/run-data/test.sched | ||
124 | [Exp test/test.sched]: Stopping tracers | ||
125 | [Exp test/test.sched]: Switching to Linux scheduler | ||
126 | [Exp test/test.sched]: Experiment done! | ||
127 | Experiments run: 1 | ||
128 | Successful: 1 | ||
129 | Failed: 0 | ||
130 | Already Done: 0 | ||
131 | Invalid environment: 0 | ||
132 | |||
133 | ``` | ||
134 | |||
135 | The following will write a release master using `/proc/litmus/release_master`: | ||
136 | |||
137 | ```bash | ||
139 | $ echo "release_master{2} | 138 | $ echo "release_master{2} |
140 | 10 20" > test.sched && run_exps.py -s GSN-EDF test.sched | 139 | 10 20" > test.sched && run_exps.py -s GSN-EDF test.sched |
140 | ``` | ||
141 | 141 | ||
142 | A longer form can be used for proc entries not in /proc/litmus: | 142 | A longer form can be used for proc entries not under `/proc/litmus`: |
143 | 143 | ||
144 | ```bash | ||
144 | $ echo "/proc/sys/something{hello}" | 145 | $ echo "/proc/sys/something{hello}" |
145 | 10 20" > test.sched | 146 | 10 20" > test.sched |
147 | ``` | ||
146 | 148 | ||
147 | You can specify your own spin programs to run as well instead of | 149 | You can specify your own spin programs to run as well instead of rtspin by putting their name at the beginning of the line. This example also shows how you can reference files in the same directory as the schedule file on the command line. |
148 | rtspin by putting their name at the beginning of the line. | ||
149 | 150 | ||
151 | ```bash | ||
150 | $ echo "colorspin -f color1.csv 10 20" > test.sched | 152 | $ echo "colorspin -f color1.csv 10 20" > test.sched |
153 | ``` | ||
151 | 154 | ||
152 | This example also shows how you can reference files in the same | 155 | You can specify parameters for an experiment in a file instead of on the command line using params.py (the `-p` option lets you choose the name of this file if params.py is not for you): |
153 | directory as the schedule file on the command line. | ||
154 | |||
155 | You can specify parameters for an experiment in a file instead of on | ||
156 | the command line using params.py (the -p option lets you choose the | ||
157 | name of this file if params.py is not for you): | ||
158 | 156 | ||
157 | ```bash | ||
159 | $ echo "{'scheduler':'GSN-EDF', 'duration':10}" > params.py | 158 | $ echo "{'scheduler':'GSN-EDF', 'duration':10}" > params.py |
160 | $ run_exps.py test.sched | 159 | $ run_exps.py test.sched |
160 | ``` | ||
161 | 161 | ||
162 | You can also run multiple experiments with a single command, provided | 162 | You can also run multiple experiments with a single command, provided a directory with a schedule file exists for each. By default, the program will look for sched.py for the schedule file and params.py for the parameter file, but this behavior can be changed using the `-p` and `-c` options. |
163 | a directory with a schedule file exists for each. By default, the | ||
164 | program will look for sched.py for the schedule file and params.py for | ||
165 | the parameter file, but this behavior can be changed using the -p and | ||
166 | -c options. | ||
167 | |||
168 | You can include non-relevant parameters which run_exps.py does not | ||
169 | understand in params.py. These parameters will be saved with the data | ||
170 | output by run_exps.py. This is useful for tracking variations in | ||
171 | system parameters versus experimental results. | ||
172 | 163 | ||
173 | In the following example, multiple experiments are demonstrated and an | 164 | You can include non-relevant parameters which `run_exps.py` does not understand in `params.py`. These parameters will be saved with the data output by `run_exps.py`. This is useful for tracking variations in system parameters versus experimental results. In the following example, multiple experiments are demonstrated and an extra parameter `test-param` is included: |
174 | extra parameter 'test-param' is included: | ||
175 | 165 | ||
166 | ```bash | ||
176 | $ mkdir test1 | 167 | $ mkdir test1 |
177 | # The duration will default to 30 and need not be specified | 168 | # The duration will default to 30 and need not be specified |
178 | $ echo "{'scheduler':'C-EDF', 'test-param':1} > test1/params.py | 169 | $ echo "{'scheduler':'C-EDF', 'test-param':1} > test1/params.py |
@@ -180,31 +171,42 @@ $ echo "10 20" > test1/sched.py | |||
180 | $ cp -r test1 test2 | 171 | $ cp -r test1 test2 |
181 | $ echo "{'scheduler':'GSN-EDF', 'test-param':2}"> test2/params.py | 172 | $ echo "{'scheduler':'GSN-EDF', 'test-param':2}"> test2/params.py |
182 | $ run_exps.py test* | 173 | $ run_exps.py test* |
174 | ``` | ||
183 | 175 | ||
184 | Finally, you can specify system properties in params.py which the | 176 | Finally, you can specify system properties in `params.py` which the environment must match for the experiment to run. These are useful if you have a large batch of experiments which must be run under different kernels or kernel configurations. The first property is a regular expression for the name of the kernel:Invalid environment for experiment 'test.sched' |
185 | environment must match for the experiment to run. These are useful if | 177 | Kernel name does not match '.*linux.*'. |
186 | you have a large batch of experiments which must be run under | 178 | Experiments run: 1 |
187 | different kernels. The first property is a regular expression for the | 179 | Successful: 0 |
188 | uname of the system: | 180 | Failed: 0 |
181 | Already Done: 0 | ||
182 | Invalid Environment: 1 | ||
189 | 183 | ||
184 | ```bash | ||
190 | $ uname -r | 185 | $ uname -r |
191 | 3.0.0-litmus | 186 | 3.0.0-litmus |
192 | $ cp params.py old_params.py | 187 | $ cp params.py old_params.py |
193 | $ echo "{'uname': r'.*linux.*'}" >> params.py | 188 | $ echo "{'uname': r'.*linux.*'}" >> params.py |
194 | # run_exps.py will now complain of an invalid environment for this | 189 | $ run_exps.py -s GSN-EDF test.sched |
195 | experiment | 190 | Invalid environment for experiment 'test.sched' |
191 | Kernel name does not match '.*linux.*'. | ||
192 | Experiments run: 1 | ||
193 | Successful: 0 | ||
194 | Failed: 0 | ||
195 | Already Done: 0 | ||
196 | Invalid Environment: 1 | ||
196 | $ cp old_params.py params.py | 197 | $ cp old_params.py params.py |
197 | $ echo "{'uname': r'.*litmus.*'}" >> params.py | 198 | $ echo "{'uname': r'.*litmus.*'}" >> params.py |
198 | # run_exps.py will now succeed | 199 | # run_exps.py will now succeed |
200 | ``` | ||
199 | 201 | ||
200 | The second property are kernel configuration options. These assume the | 202 | The second property is kernel configuration options. These assume the configuration is stored at `/boot/config-```uname -r`` `. You can specify these like so: |
201 | configuration is stored at /boot/config-`uname -r`. You can specify | ||
202 | these like so: | ||
203 | 203 | ||
204 | ```bash | ||
205 | # Only executes on ARM systems with the release master enabled | ||
204 | $ echo "{'config-options':{ | 206 | $ echo "{'config-options':{ |
205 | 'RELEASE_MASTER' : 'y', | 207 | 'RELEASE_MASTER' : 'y', |
206 | 'ARM' : 'y'}}" >> params.py | 208 | 'ARM' : 'y'}}" >> params.py |
207 | # Only executes on ARM systems with the release master enabled | 209 | ``` |
208 | 210 | ||
209 | 211 | ||
210 | III. GEN_EXPS | 212 | III. GEN_EXPS |
@@ -505,3 +507,9 @@ However, when a single directory of directories is given, the script | |||
505 | assumes the experiments are related and can make line styles match in | 507 | assumes the experiments are related and can make line styles match in |
506 | different plots and more effectively parallelize the plotting. | 508 | different plots and more effectively parallelize the plotting. |
507 | 509 | ||
510 | [litmus]: https://github.com/LITMUS-RT/litmus-rt | ||
511 | [liblitmus]: https://github.com/LITMUS-RT/liblitmus | ||
512 | [rt-kernelshark]: https://github.com/LITMUS-RT/rt-kernelshark | ||
513 | [feather-trace-tools]: https://github.com/LITMUS-RT/feather-trace-tools | ||
514 | [rtunc]: http://www.cs.unc.edu/~anderson/real-time/ | ||
515 | [matplotlib]: http://matplotlib.org/ \ No newline at end of file | ||
diff --git a/run_exps.py b/run_exps.py index dc15701..6873877 100755 --- a/run_exps.py +++ b/run_exps.py | |||
@@ -15,12 +15,11 @@ from run.experiment import Experiment,ExperimentDone | |||
15 | from run.proc_entry import ProcEntry | 15 | from run.proc_entry import ProcEntry |
16 | 16 | ||
17 | class InvalidKernel(Exception): | 17 | class InvalidKernel(Exception): |
18 | def __init__(self, kernel, wanted): | 18 | def __init__(self, kernel): |
19 | self.kernel = kernel | 19 | self.kernel = kernel |
20 | self.wanted = wanted | ||
21 | 20 | ||
22 | def __str__(self): | 21 | def __str__(self): |
23 | return "Kernel '%s' does not match '%s'." % (self.kernel, self.wanted) | 22 | return "Kernel name does not match '%s'." % self.kernel |
24 | 23 | ||
25 | ConfigResult = namedtuple('ConfigResult', ['param', 'wanted', 'actual']) | 24 | ConfigResult = namedtuple('ConfigResult', ['param', 'wanted', 'actual']) |
26 | class InvalidConfig(Exception): | 25 | class InvalidConfig(Exception): |
@@ -119,7 +118,7 @@ def load_experiment(sched_file, scheduler, duration, param_file, out_dir): | |||
119 | exp_name = os.path.split(dir_name)[1] + "/" + fname | 118 | exp_name = os.path.split(dir_name)[1] + "/" + fname |
120 | 119 | ||
121 | params = {} | 120 | params = {} |
122 | kernel = "" | 121 | kernel = copts = "" |
123 | 122 | ||
124 | param_file = param_file or \ | 123 | param_file = param_file or \ |
125 | "%s/%s" % (dir_name, conf.DEFAULTS['params_file']) | 124 | "%s/%s" % (dir_name, conf.DEFAULTS['params_file']) |
@@ -259,7 +258,7 @@ def main(): | |||
259 | print("Experiment '%s' already completed at '%s'" % (exp, out_base)) | 258 | print("Experiment '%s' already completed at '%s'" % (exp, out_base)) |
260 | except (InvalidKernel, InvalidConfig) as e: | 259 | except (InvalidKernel, InvalidConfig) as e: |
261 | invalid += 1 | 260 | invalid += 1 |
262 | print("Invalid environment for experiment '%s'") | 261 | print("Invalid environment for experiment '%s'" % exp) |
263 | print(e) | 262 | print(e) |
264 | except: | 263 | except: |
265 | print("Failed experiment %s" % exp) | 264 | print("Failed experiment %s" % exp) |
@@ -273,7 +272,7 @@ def main(): | |||
273 | print(" Successful:\t\t%d" % succ) | 272 | print(" Successful:\t\t%d" % succ) |
274 | print(" Failed:\t\t%d" % failed) | 273 | print(" Failed:\t\t%d" % failed) |
275 | print(" Already Done:\t\t%d" % done) | 274 | print(" Already Done:\t\t%d" % done) |
276 | print(" Invalid environment:\t\t%d" % invalid) | 275 | print(" Invalid Environment:\t%d" % invalid) |
277 | 276 | ||
278 | 277 | ||
279 | if __name__ == '__main__': | 278 | if __name__ == '__main__': |