9aa5dbd4b4eb8d08d318596f18092c7262597892
[alexxy/gromacs.git] / python_packaging / src / test / test_commandline.py
1 #!/usr/bin/env python
2 #
3 # This file is part of the GROMACS molecular simulation package.
4 #
5 # Copyright (c) 2019,2020, by the GROMACS development team, led by
6 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
7 # and including many others, as listed in the AUTHORS file in the
8 # top-level source directory and at http://www.gromacs.org.
9 #
10 # GROMACS is free software; you can redistribute it and/or
11 # modify it under the terms of the GNU Lesser General Public License
12 # as published by the Free Software Foundation; either version 2.1
13 # of the License, or (at your option) any later version.
14 #
15 # GROMACS is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18 # Lesser General Public License for more details.
19 #
20 # You should have received a copy of the GNU Lesser General Public
21 # License along with GROMACS; if not, see
22 # http://www.gnu.org/licenses, or write to the Free Software Foundation,
23 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
24 #
25 # If you want to redistribute modifications to GROMACS, please
26 # consider that scientific software is very special. Version
27 # control is crucial - bugs must be traceable. We will be happy to
28 # consider code for inclusion in the official distribution, but
29 # derived work must not be called official GROMACS. Details are found
30 # in the README & COPYING files - if they are missing, get the
31 # official version at http://www.gromacs.org.
32 #
33 # To help us fund GROMACS development, we humbly ask that you cite
34 # the research papers on the package. Check out http://www.gromacs.org.
35
36 """Tests for dynamically defined operations.
37
38 Test the command-line wrapping functionality in gmxapi.commandline. These
39 operation factories are written with user-facing tools and exercise a lot of
40 the high-level machinery of the package, effectively serving as integration
41 tests of the operation-building utilities in the modules depended on by
42 commandline.py.
43 """
44
45 import os
46 import shutil
47 import stat
48 import unittest
49
50 import gmxapi as gmx
51 import pytest
52 from gmxapi import commandline
53
54
55 class SimpleCliTestCase(unittest.TestCase):
56     """Test creation and execution of the basic cli() command line wrapper."""
57
58     def test_true(self):
59         """Test a command known to produce a return code of 0."""
60         command = shutil.which('true')
61         operation = commandline.cli(command=[command], shell=False)
62
63         # Note: 'stdout' and 'stderr' not mapped.
64         # Note: getitem not implemented.
65         # assert not 'stdout' in operation.output
66         # assert not 'stderr' in operation.output
67         assert not hasattr(operation.output, 'stdout')
68         assert not hasattr(operation.output, 'stderr')
69
70         # Check for the attributes that we _do_ expect.
71         assert hasattr(operation.output, 'erroroutput')
72         assert hasattr(operation.output, 'returncode')
73
74         operation.run()
75         # assert operation.output.returncode.result() == 0
76         assert operation.output.returncode.result() == 0
77
78     def test_false_explicit(self):
79         """Test a command known to produce a return code of 1."""
80         command = shutil.which('false')
81         operation = commandline.cli(command=[command], shell=False)
82         # Explicitly run the operation.
83         operation.run()
84         assert operation.output.returncode.result() == 1
85
86     def test_false_implicit(self):
87         command = shutil.which('false')
88         operation = commandline.cli(command=[command], shell=False)
89         # Allow the operation to be executed implicitly to satisfy data constraint.
90         assert operation.output.returncode.result() == 1
91
92     def test_command_with_arguments(self):
93         """Test that cli() can wrap a command with arguments."""
94         # TODO: (FR5+) do we want to pipeline or checkpoint stdout somehow?
95         operation = commandline.cli(command=[shutil.which('echo'), 'hi', 'there'], shell=False)
96         assert operation.output.returncode.result() == 0
97
98     def test_command_with_stdin(self):
99         """Test that cli() can handle string input."""
100         stdin = 'hi\nthere\n'
101         subcommand = '{wc} -l | {grep} -q 2'.format(wc=shutil.which('wc'), grep=shutil.which('grep'))
102
103         operation = commandline.cli(command=['/bin/sh', '-c', subcommand], shell=False, stdin=stdin)
104         assert operation.output.returncode.result() == 0
105         operation = commandline.commandline_operation('/bin/sh', ['-c', subcommand], stdin=stdin)
106         assert operation.output.returncode.result() == 0
107
108         subcommand = '{wc} -l | {grep} -q 1'.format(wc=shutil.which('wc'), grep=shutil.which('grep'))
109
110         operation = commandline.cli(command=['/bin/sh', '-c', subcommand], shell=False, stdin=stdin)
111         assert operation.output.returncode.result() != 0
112         operation = commandline.commandline_operation('/bin/sh', ['-c', subcommand], stdin=stdin)
113         assert operation.output.returncode.result() != 0
114
115
116 class CommandLineOperationSimpleTestCase(unittest.TestCase):
117     """Test the command line wrapper operation factory."""
118
119     def test_true(self):
120         operation = commandline.commandline_operation(executable='true')
121         # Note: 'stdout' and 'stderr' not mapped.
122         # Note: getitem not implemented.
123         # assert not 'stdout' in operation.output
124         # assert not 'stderr' in operation.output
125         assert not hasattr(operation.output, 'stdout')
126         assert not hasattr(operation.output, 'stderr')
127         assert hasattr(operation.output, 'file')
128         assert hasattr(operation.output, 'erroroutput')
129         assert hasattr(operation.output, 'returncode')
130         assert operation.output.returncode.result() == 0
131
132     def test_false(self):
133         operation = commandline.commandline_operation(executable='false')
134         assert operation.output.returncode.result() == 1
135
136     def test_echo(self):
137         # TODO: (FR5+) do we want to pipeline or checkpoint stdout somehow?
138         operation = commandline.commandline_operation(executable='echo',
139                                                       arguments=['hi there'])
140         assert operation.output.returncode.result() == 0
141
142
143 def test_file_dependency_chain(cleandir):
144     """Test the command line wrapper input/output file handling.
145
146     Operation output can be used as operation input.
147     """
148     file1 = os.path.join(cleandir, 'input')
149     file2 = os.path.join(cleandir, 'output')
150
151     # Make a shell script that acts like the type of tool we are wrapping.
152     scriptname = os.path.join(cleandir, 'clicommand.sh')
153     with open(scriptname, 'w') as fh:
154         fh.write('\n'.join(['#!' + shutil.which('bash'),
155                             '# Concatenate an input file and a string argument to an output file.',
156                             '# Mock a utility with the tested syntax.',
157                             '#     clicommand.sh "some words" -i inputfile -o outputfile',
158                             'echo $1 | cat $3 - > $5\n']))
159     os.chmod(scriptname, stat.S_IRWXU)
160
161     line1 = 'first line'
162     filewriter1 = gmx.commandline_operation(scriptname,
163                                             arguments=[line1],
164                                             input_files={'-i': os.devnull},
165                                             output_files={'-o': file1})
166
167     line2 = 'second line'
168     filewriter2 = gmx.commandline_operation(scriptname,
169                                             arguments=[line2],
170                                             input_files={'-i': filewriter1.output.file['-o']},
171                                             output_files={'-o': file2})
172
173     filewriter2.run()
174     # Check that the files have the expected lines
175     with open(file1, 'r') as fh:
176         lines = [text.rstrip() for text in fh]
177     assert len(lines) == 1
178     assert lines[0] == line1
179     with open(file2, 'r') as fh:
180         lines = [text.rstrip() for text in fh]
181     assert len(lines) == 2
182     assert lines[0] == line1
183     assert lines[1] == line2
184
185 def test_failure(cleandir):
186     """The operation should not deliver file output if the subprocess fails."""
187     file1 = os.path.join(cleandir, 'input')
188     file2 = os.path.join(cleandir, 'output')
189
190     # Make a shell script that acts like the type of tool we are wrapping.
191     scriptname = os.path.join(cleandir, 'clicommand.sh')
192     with open(scriptname, 'w') as fh:
193         fh.write('\n'.join(['#!' + shutil.which('bash'),
194                             '# Concatenate an input file and a string argument to an output file.',
195                             '# Mock a utility with the tested syntax.',
196                             '#     clicommand.sh "some words" -i inputfile -o outputfile',
197                             'exit 1\n']))
198     os.chmod(scriptname, stat.S_IRWXU)
199
200     filewriter1 = gmx.commandline_operation(scriptname,
201                                             input_files={'-i': os.devnull},
202                                             output_files={'-o': file1})
203
204     filewriter2 = gmx.commandline_operation(scriptname,
205                                             input_files={'-i': filewriter1.output.file['-o']},
206                                             output_files={'-o': file2})
207
208     # filewriter1 has a non-zero exit code and should have no output files available.
209     with pytest.raises(KeyError):
210         filewriter2.run()
211
212
213 if __name__ == '__main__':
214     unittest.main()