Merge pull request #48658 from wyardley/wyardley-npm-json-output-2017

Improve handling of json output (#43138)
This commit is contained in:
Nicole Thomas 2018-07-20 10:08:33 -04:00 committed by GitHub
commit 93d2f51d2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 66 additions and 41 deletions

View file

@ -160,7 +160,11 @@ def install(pkg=None,
env.update({'SUDO_UID': uid, 'SUDO_USER': ''})
cmd = ' '.join(cmd)
result = __salt__['cmd.run_all'](cmd, python_shell=True, cwd=dir, runas=runas, env=env)
result = __salt__['cmd.run_all'](cmd,
python_shell=True,
cwd=dir,
runas=runas,
env=env)
if result['retcode'] != 0:
raise CommandExecutionError(result['stderr'])
@ -168,33 +172,9 @@ def install(pkg=None,
# npm >1.2.21 is putting the output to stderr even though retcode is 0
npm_output = result['stdout'] or result['stderr']
try:
return json.loads(npm_output)
return salt.utils.find_json(npm_output)
except ValueError:
pass
json_npm_output = _extract_json(npm_output)
return json_npm_output or npm_output
def _extract_json(npm_output):
lines = npm_output.splitlines()
log.error(lines)
# Strip all lines until JSON output starts
while lines and not lines[0].startswith('{') and not lines[0].startswith('['):
lines = lines[1:]
while lines and not lines[-1].startswith('}') and not lines[-1].startswith(']'):
lines = lines[:-1]
# macOS with fsevents includes the following line in the return
# when a new module is installed which is invalid JSON:
# [fsevents] Success: "..."
while lines and (lines[0].startswith('[fsevents]') or lines[0].startswith('Pass ')):
lines = lines[1:]
try:
return json.loads(''.join(lines))
except ValueError:
pass
return None
return npm_output
def uninstall(pkg, dir=None, runas=None, env=None):

View file

@ -6,6 +6,7 @@
# Import Python Libs
from __future__ import absolute_import
import json
import textwrap
# Import Salt Testing Libs
from tests.support.mixins import LoaderModuleMockMixin
@ -34,43 +35,87 @@ class NpmTestCase(TestCase, LoaderModuleMockMixin):
self.addCleanup(patcher.stop)
return {npm: {}}
# 'install' function tests: 1
# 'install' function tests: 4
def test_install(self):
'''
Test if it install an NPM package.
Test if it installs an NPM package.
'''
mock = MagicMock(return_value={'retcode': 1, 'stderr': 'error'})
with patch.dict(npm.__salt__, {'cmd.run_all': mock}):
self.assertRaises(CommandExecutionError, npm.install,
'coffee-script')
mock = MagicMock(return_value={'retcode': 0, 'stderr': 'error',
'stdout': '{"salt": ["SALT"]}'})
with patch.dict(npm.__salt__, {'cmd.run_all': mock}):
mock_err = MagicMock(return_value='SALT')
with patch.object(json, 'loads', mock_err):
self.assertEqual(npm.install('coffee-script'), 'SALT')
# This is at least somewhat closer to the actual output format.
mock_json_out = textwrap.dedent('''\
[
{
"salt": "SALT"
}
]''')
mock = MagicMock(return_value={'retcode': 0, 'stderr': 'error',
'stdout': '{"salt": ["SALT"]}'})
# Successful run, expected output format
mock = MagicMock(return_value={'retcode': 0, 'stderr': '',
'stdout': mock_json_out})
with patch.dict(npm.__salt__, {'cmd.run_all': mock}):
self.assertEqual(npm.install('coffee-script'),
[{u'salt': u'SALT'}])
mock_json_out_extra = textwrap.dedent('''\
Compilation output here
[bcrypt] Success: "/tmp/node_modules/bcrypt/foo" is installed via remote"
[grpc] Success: "/usr/lib/node_modules/@foo/bar" is installed via remote"
[
{
"from" : "express@",
"name" : "express",
"dependencies" : {
"escape-html" : {
"from" : "escape-html@~1.0.3",
"dependencies" : {},
"version" : "1.0.3"
}
},
"version" : "4.16.3"
}
]''')
extra_expected = [{u'dependencies':
{u'escape-html': {
u'dependencies': {},
u'from': u'escape-html@~1.0.3',
u'version': u'1.0.3'}
},
u'from': u'express@',
u'name': u'express',
u'version': u'4.16.3'}]
# Successful run, expected output format with additional leading text
mock = MagicMock(return_value={'retcode': 0, 'stderr': '',
'stdout': mock_json_out_extra})
with patch.dict(npm.__salt__, {'cmd.run_all': mock}):
self.assertEqual(npm.install('coffee-script'), extra_expected)
# Successful run, unexpected output format
mock = MagicMock(return_value={'retcode': 0, 'stderr': '',
'stdout': 'SALT'})
with patch.dict(npm.__salt__, {'cmd.run_all': mock}):
mock_err = MagicMock(side_effect=ValueError())
# When JSON isn't successfully parsed, return should equal input
with patch.object(json, 'loads', mock_err):
self.assertEqual(npm.install('coffee-script'),
'{"salt": ["SALT"]}')
self.assertEqual(npm.install('coffee-script'), 'SALT')
# 'uninstall' function tests: 1
def test_uninstall(self):
'''
Test if it uninstall an NPM package.
Test if it uninstalls an NPM package.
'''
mock = MagicMock(return_value={'retcode': 1, 'stderr': 'error'})
with patch.dict(npm.__salt__, {'cmd.run_all': mock}):
self.assertFalse(npm.uninstall('coffee-script'))
mock = MagicMock(return_value={'retcode': 0, 'stderr': 'error'})
mock = MagicMock(return_value={'retcode': 0, 'stderr': ''})
with patch.dict(npm.__salt__, {'cmd.run_all': mock}):
self.assertTrue(npm.uninstall('coffee-script'))