-
Notifications
You must be signed in to change notification settings - Fork 78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Read target connection settings from a YAML file #670
Conversation
2096387
to
cd7bea9
Compare
devlib/utils/misc.py
Outdated
@@ -37,6 +37,7 @@ | |||
import types | |||
import warnings | |||
import wrapt | |||
import yaml |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't remember the specifics but there is a fundamental design issue with that library. I used ruamel.yaml in Lisa instead. That might have to do with a mutable singleton used in the lib for something central that could conflict between different users in the same process.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember now, it's this pattern that is bonkers:
import yaml
yaml.add_constructor()
So any other dependency can decide to change the behavior of pyyaml and everyone in the same process will get the change of behavior, devlib included. That may come from the underlying C library that this wraps but this is the best recipe for unsolvable issues.
Ruamel.yaml added a way to get some sort of context object and custom constructors are only registered against it. Every user has their own instance and no-one is stepping on each other's toes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the hint. Move from yaml
to ruamel
.
cd7bea9
to
7756317
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not a deep review on the added code, just superficial comments
@@ -590,6 +591,31 @@ def __str__(self): | |||
return message.format(self.filepath, self.lineno, self.message) | |||
|
|||
|
|||
def load_struct_from_yaml(filepath): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now we don't want the testing infrastructure to be a public API, so this needs a non-public name starting with a single underscore (https://peps.python.org/pep-0008/)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
load_struct_from_yaml()
is not for testing purposes only.
It's just moved from WA to devlib: ARM-software/workload-automation#1248
devlib/utils/misc.py
Outdated
Args: | ||
filepath (str): Input file which contains YAML data. | ||
|
||
Raises: | ||
LoadSyntaxError: if there is a syntax error in YAML data. | ||
|
||
Returns: | ||
dict: which contains parsed YAML data. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would recommend using the autodoc style as this is the most native one with Sphinx (you can see what we use in LISA)
except yaml.YAMLError as ex: | ||
message = ex.message if hasattr(ex, 'message') else '' | ||
lineno = ex.problem_mark.line if hasattr(ex, 'problem_mark') else None | ||
raise LoadSyntaxError(message, filepath=filepath, lineno=lineno) from ex |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what LoadSyntaxError was initially for. Currently it seems like dead code in devlib. Unless it really adds something, I'd recommend just letting the YAMLError bubble up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LoadSyntaxError()
is referenced in load_struct_from_python()
, too.
I don't know the history, but apparently it makes the syntax error location more explicit, IMHO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use this in WA to try and provide a more friendly error message to clearly indicate the location of the syntax error.
Since this code is being moved, I'd vote for keeping the same error class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, I did not look beyond devlib codebase
tests/test_target.py
Outdated
@@ -14,17 +14,30 @@ | |||
# limitations under the License. | |||
# | |||
|
|||
''' Module for testing targets. ''' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docstrings should generally be trimmed, no space at the beginning or the end (unless it's needed for indentation after a newline)
tests/test_target.py
Outdated
|
||
|
||
class TestReadTreeValues(TestCase): | ||
''' Class testing Target.read_tree_values_flat() ''' | ||
|
||
TARGETS_CONFIG_FILE = 'tests/target_configs.yaml' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does pytest guarantees the current folder when running tests ? If not this could be made relative to __file__
's folder for example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I run tests under /devlib
dir. Will use __file__
's folder to ensure that.
tests/test_target.py
Outdated
|
||
def test_read_multiline_values(self): | ||
''' Test Target.read_tree_values_flat() ''' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/^ //
and s/ $//
tests/test_target.py
Outdated
@@ -34,11 +47,13 @@ def test_read_multiline_values(self): | |||
tempdir = tempfile.mkdtemp(prefix='devlib-test-') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should use context manager classes available in tempfile
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a change here in this PR.
79a91b7 uses context manager anyway.
devlib/utils/android.py
Outdated
@@ -294,6 +294,10 @@ def __init__( | |||
total_transfer_timeout=total_transfer_timeout, | |||
transfer_poll_period=transfer_poll_period, | |||
) | |||
|
|||
self.logger.debug('AdbConnection(): server=%s port=%s device=%s as_root=%s', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/AdbConnection(): //
The name of the class should be used when the logger is made.
devlib/utils/android.py
Outdated
@@ -294,6 +294,10 @@ def __init__( | |||
total_transfer_timeout=total_transfer_timeout, | |||
transfer_poll_period=transfer_poll_period, | |||
) | |||
|
|||
self.logger.debug('AdbConnection(): server=%s port=%s device=%s as_root=%s', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a slight technical benefit of using this way of interpolating (i.e. avoiding interpolation unless that debug level is enabled) but it's negligible and mostly just increases the risk of mixup. You can use an f-string instead to avoid that, and even let the compiler avoid redundancy with e.g. {adb_server=}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think pylint
recommended using lazy formatting instead of f-string.
While we are there, also fix a trivial pylint issue regarding string format. Signed-off-by: Metin Kaya <[email protected]>
b9ffd3f
to
49f1b39
Compare
This is copied from WA (workload-automation/wa/utils/misc.py). Hence, published another PR [1] removes the implementation from WA. OTOH, this patch uses ``ruamel`` instead of ``yaml`` because of the latter's design issues. And also this patch fixes pylint issues in ``load_struct_from_yaml()``. [1] ARM-software/workload-automation#1248 Signed-off-by: Metin Kaya <[email protected]>
49f1b39
to
36731c5
Compare
except yaml.YAMLError as ex: | ||
message = ex.message if hasattr(ex, 'message') else '' | ||
lineno = ex.problem_mark.line if hasattr(ex, 'problem_mark') else None | ||
raise LoadSyntaxError(message, filepath=filepath, lineno=lineno) from ex |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use this in WA to try and provide a more friendly error message to clearly indicate the location of the syntax error.
Since this code is being moved, I'd vote for keeping the same error class.
tests/test_target.py
Outdated
|
||
|
||
class TestReadTreeValues(TestCase): | ||
"""Class testing Target.read_tree_values_flat()""" | ||
|
||
TARGETS_CONFIG_FILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the intention is to be able to perform these tests on different device types based on the config, do we want the parsing / initialization to be performed within each test case?
Should these actions be performed in some setup method instead that can be reused?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The final product is something like this: a7d0b9d
Yes, there are different target types and there can be multiple entries for a target.
I read config file first and see which targets need to be tested. So, I could keep target classes as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm.. I see. Apologies in advanced if I'm missing the obvious but in the final product do we end up with this code just for a single test_read_multiline_values
unittest?
Does this mean this will need to be duplicated when we add another test or test Class?
Also does this require setting up a new connection to the target for each test? I'm wondering what the overhead/performance would be on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is only one test_read_multiline_values()
function. It parses the YAML file and executes the test specified in run_test()
for each target in the YAML file. YAML file has necessary connection information and QEMU params for each target. Depending on the target type, running run_test()
can take ~1 (LocalLinux target) or ~15 secs (for QEMU target).
Final version of TestReadTreeValues()
looks like this:
class TestReadTreeValues(TestCase):
def test_read_multiline_values(self):
def run_test(target):
...
target_configs = load_struct_from_yaml(self.TARGETS_CONFIG_FILE)
...
if target_configs.get('AndroidTarget') is not None:
...
target = AndroidTarget(
connection_settings=entry['connection_settings'],
conn_cls=lambda **kwargs: AdbConnection(adb_as_root=True, **kwargs),
)
run_test(target)
if target_configs.get('LinuxTarget') is not None:
...
target = LinuxTarget(connection_settings=entry['connection_settings'])
run_test(target)
if target_configs.get('LocalLinuxTarget') is not None:
...
target = LocalLinuxTarget(connection_settings=entry['connection_settings'])
run_test(target)
if target_configs.get('QEMUTargetRunner') is not None:
...
with QEMUTargetRunner(
qemu_params=qemu_params,
connection_settings=connection_settings,
) as qemu_target:
run_test(qemu_target.target)
And this is a sample YAML file:
AndroidTarget:
entry-0:
connection_settings:
device: 'emulator-5554'
entry-1:
connection_settings:
device: 'emulator-5556'
LinuxTarget:
entry-0:
connection_settings:
host: 'example.com'
username: 'username'
password: 'password'
LocalLinuxTarget:
entry-0:
connection_settings:
unrooted: True
QEMUTargetRunner:
entry-0:
qemu_params:
kernel_image: '/devlib/tools/buildroot/buildroot-v2023.11.1-aarch64/output/images/Image'
entry-1:
qemu_params:
kernel_image: '/devlib/tools/buildroot/buildroot-v2023.11.1-x86_64/output/images/bzImage'
arch: 'x86_64'
cmdline: 'console=ttyS0'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, thanks for the information.
Although we have only have a single test case test_read_multiline_values
at the moment, if we have this nice infrastructure to be able to run these tests and have more meaningful output, I'm thinking if we were expecting to add more test cases going forward, how would that fit into the current implementation? For example if we wanted to add a test_read_escape_values
test case that checks certain characters are escaped correctly.
The parsing / creation of the various targets looks like it should be mostly generic and not tied to this single test case, therefore I'm wondering if there is a common location this would be better suited to be shared?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That totally makes sense to me.
How about the new version? If it looks OK, I'm gonna update other PRs.
Moving to pytest
is the simplest solution I could find to prevent duplicate target config reading, passing target parameter to the test function (as well as future ones).
Thanks.
aefcf76
to
fc0c531
Compare
This will be useful in automating CI tests without modifying the source code. Replace unittest with pytest in order to make parameter passing to test functions easier. Move target configuration reading and generating target object outside of the test function. Because we will run the test function for new targets and may want to add new test functions. While we are here, also fix pylint issues. Signed-off-by: Metin Kaya <[email protected]>
fc0c531
to
6217c77
Compare
path = os.path.join(tempdir, key) | ||
with open(path, 'w') as wfh: | ||
wfh.write(value) | ||
tempdir = tempfile.mkdtemp(prefix='devlib-test-') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should use tempfile.TemporaryDirectory, as a context manager.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Latest version of this code:
with target.make_temp() as tempdir:
...
raw_result = target.read_tree_values_flat(tempdir)
result = {os.path.basename(k): v for k, v in raw_result.items()}
And this is Target()::make_temp()
implementation (295f126):
@contextmanager
def make_temp(self, is_directory=True, directory='', prefix='devlib-test'):
...
try:
cmd = f'mktemp -p {directory} {prefix}-XXXXXX'
if is_directory:
cmd += ' -d'
temp_obj = self.execute(cmd).strip()
yield temp_obj
finally:
if temp_obj is not None:
self.remove(temp_obj)
Commits:
PRs #667 and ARM-software/workload-automation#1248 can be merged after this one.