horizon/tools/unit_tests.sh
Stephen Finucane a11c3d3161 Don't modify Resource fields in place
Attribute accesses in SDK pass through a type-conversion utility method
that is responsible for ensuring the following behavior:

  >>> class Demo(resource.Resource):
  ...     foo = resource.Body('foo', type=str)
  ...
  >>> demo = Demo(foo=123)
  >>> demo.foo
  '123'  # result is cast to a string

Unfortunately, because of how this is implemented, attribute accesses
can result result in a copy of the attribute being returned, rather than
the attribute itself. This means attempts to modify mutable attributes
in-place can end up modifying a copy of the attributes, rather than the
attribute itself.

  >>> class Demo(resource.Resource):
  ...     foo = resource.Body('foo', type=list, list_type=str)
  ...
  >>> demo = Demo(foo=[123])
  >>> demo.foo
  ['123']  # items are cast to strings
  >>> demo.foo.append(456)
  >>> demo.foo
  ['123']  # 456 is missing!

This was not previously an issue for horizon as we were not hitting any
of these cases, however, in in openstacksdk 4.3.0, a bug was addressed
where conversion would not happen if using 'type=list', if 'list_type'
was unset, and if the item was a set or tuple (i.e. not a list).

  >>> class Demo(resource.Resource):
  ...     foo = resource.Body('foo', type=list)
  ...
  >>> demo = Demo(foo={'123'})
  >>> demo.foo
  {'123'}  # should be a list!

This has now been fixed, however, the bugfix has exposed a case where we
were in fact modifying a mutable list in-place.

The long-term fix lies in changing how openstacksdk works, so that it
always return the original attribute rather potentially returning a
copy. However, this fix is likely going to be rather involved and not
something we want to cram into the end of the Epoxy release. Instead,
modify Horizon so that it updates the entire attribute rather than
modifying it in place.

Change-Id: I6e7814ea16ca84689b363a53f22de62800c1f0d8
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Related-bug: #2100605
2025-02-28 13:49:57 +00:00

101 lines
2.2 KiB
Bash
Executable File

# Usage: unit_tests.sh [--coverage] <root_dir> [<test-file>, ...]
set -o xtrace
if [ "$1" = "--coverage" ]; then
shift
coverage=1
else
coverage=0
fi
root=$1
posargs="${@:2}"
report_dir=$root/test_reports
# Attempt to identify if any of the arguments passed from tox is a test subset
if [ -n "$posargs" ]; then
for arg in "$posargs"
do
if [ ${arg:0:1} != "-" ]; then
subset=$arg
fi
done
fi
function run_test {
local project=$1
local tag
local target
local settings_module
local report_args
local ignore
tag="not selenium and not integration and not plugin_test"
case "$project" in
horizon)
settings_module="horizon.test.settings"
;;
openstack_dashboard)
settings_module="openstack_dashboard.test.settings"
;;
openstack_auth)
settings_module="openstack_auth.tests.settings"
;;
plugin|plugin-test|plugin_test)
project="plugin"
tag="plugin_test"
target="$root/openstack_dashboard/test/test_plugins"
settings_module="openstack_dashboard.test.settings"
;;
*)
# Declare error by returning 1 which usually means error in bash
return 1
esac
if [ -z "$target" ]; then
if [ -n "$subset" ]; then
target="$subset"
else
target="$root/$project"
fi
fi
ignore="--ignore=$root/openstack_dashboard/test/selenium"
if [ "$coverage" -eq 1 ]; then
coverage run -m pytest $target $ignore --ds=$settings_module -m "$tag"
else
report_args="--junitxml=$report_dir/${project}_test_results.xml"
report_args+=" --html=$report_dir/${project}_test_results.html"
report_args+=" --self-contained-html"
pytest $target --ds=$settings_module -v -m "$tag" $ignore $report_args
fi
return $?
}
# If we are running a test subset, supply the correct settings file.
# If not, simply run the entire test suite.
if [ -n "$subset" ]; then
project="${subset%%/*}"
run_test $project
exit $?
else
results=()
for project in horizon openstack_dashboard openstack_auth plugin; do
run_test $project
results+=($?)
done
# we have to tell tox if either of these test runs failed
for r in "${results[@]}"; do
if [ $r != 0 ]; then
exit 1
fi
done
fi