Skip to content
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

target: tests: Address review comments on PR#667 #677

Merged
merged 1 commit into from
May 29, 2024

Conversation

metin-arm
Copy link
Contributor

@metin-arm metin-arm commented Mar 28, 2024

PR#667: https://github.com/ARM-software/devlib/pull/667

  • Implement separate initialize / tear down methods and make various cleanups in test/test_target.py
  • Make docstrings Sphinx compatible
  • Make TargetRunner and its subclasses private
  • Cleanup tests/test_target.py
  • Replace print()'s with appropriate logging calls
  • Implement NOPTargetRunner class for simplifying tests
  • Improve Python v3.7 compatibility
  • Relax host machine type checking
  • Escape user input strings

and more..

Comment on lines 69 to 87
if runner_cmd is None:
return

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a good idea. Doing so leaves the object in a half-initialized state where some attributes may or may not exist based on some runtime condition. If that is "actually what you meant to get", this indicates either a design problem and the class hierarchy needs to be reworked (e.g. you are conflating the base class defining the interface that all instances must provide with a specific implementation that manages a Popen object), or simply that you need a no-op kind of runner_process. Most likely it's a class hierarchy issue.

In case you are tempted, leaving that as-is and editing every method that use that attribute to make them check if the attribute exists first is also a terrible idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your kind comment.
Will find a solution to clean runner_cmd check.


assert {k: v.strip() for k, v in data.items()} == result
@classmethod
def setUpClass(cls):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like pytest has better alternatives than setupClass: https://stackoverflow.com/questions/40660842/pytest-setup-teardown-hooks-for-session
setupClass is a dodgy design as the class is a singleton for the entire duration of the entire process it's in. You can't cleanly re-import a module in Python without creating problems. As a general rule, I'd advise using pytest features over the unittest module for anything else that is not really basic. unittest was designed a loooonnng time ago, as a copy/paste from some Java lib. It's not really representative of today's Python best practices (if it ever was), as you can guess from the pre-PEP8 method names.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did try to solve it in pytest, but could not find a setup and teardown methods which run exactly for once.
Will look into https://stackoverflow.com/a/40673918/1370011 to give another chance.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the fixture with module "scope" worked out


target_runners = []

if target_configs.get('AndroidTarget') is not None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The config seems to be organized as:

<target type>:
   - <settings 1>
   - <settings 2>
   - ...
 ...

I wouldn't advise doing that as this makes modularizing the configuration basically impossible. The <target type> keys are always going to be unique as per YAML spec, so by organizing it this way you prevent e.g. concatenating multiple files together. You also prevent packaging some target settings in a separate file that would then be included, as what you could include this way would only be partial information (lacking the target type).

As a result, I'd strongly recommend you use a single list of target configurations, and each item of this list is fully self-descriptive and does not rely on the context (name of the parent node) for its interpretation.

Even better, if you host the entire configuration under one top-level key, you will also make it compatible with other YAML conf, like e.g. the LISA one, allowing users to use a single file for various things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved all target configurations under a top-level key.

Comment on lines 168 to 169
logger.debug('Removing %s...', target.working_directory)
target.remove(target.working_directory)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never gotten around to making the patch but I think this should just be standard behavior of target upon disconnecting a target. @marcbonnici do you see any reason not to wipe the working_directory ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In WA we have a few workload that benefit from keeping large files (photos, videos etc.) on the device between runs to reduce the setup costs/overhead. By default most of these workloads keep their files under the working_directory so it could be undesirable to wipe this directory on each disconnect.

The general pattern we have tried to follow with our WA asset deployment mechanism is that if a files/directory is not needed after a given run is that we remove it but we still leave the option for workloads to override this as they choose.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would make sense to default to cleaning up, and have WA override that default then ? In general, keeping resources deployed on the target are a recipe for trouble unless carefully implemented (e.g. checksuming the resources to ensure what is already deployed is actually what the code expects to work with). Even if WA does that carefully to avoid troubles, I wouldn't expect most client code to go to these length. In LISA, we push all our tools every time they are needed (at most once per Target instance, so it's "cached" but only very temporarily).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, I agree it is safer in the general case to push to the target each time but even today a client can currently do so without issue. I would just question whether it makes sense for devlib to automatically handle the cleanup vs leave the decision to the client code itself via whatever mechanism they choose.

So far we haven't placed any restrictions on the expectations on the working directory, so I'd be hesitant to introduce a new default that would wipe it out as we can't say how other users / client code could be using this directory.

Essentially I think I might be missing what the benefit would be to the end user vs the potential downsides of clearing files the user may not have wanted to?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll keep this hunk as is since this discussion sounds like a separate/future improvement.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move the discussion to #680, but let's keep that comment thread open as the code will need to be changed if we end up implementing the feature (or at least leave a # TODO: revisit based on https://github.com/ARM-software/devlib/issues/680 outcome in the code).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

"""
Class for implementing a target runner which does nothing except providing .target attribute.

:class:`NOPTargetRunner` is a subclass of :class:`TargetRunner` which is implemented only for
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Autodoc makes the subclass relationship obvious, there is no need to restate it here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Will remove it.

"""

def __init__(self, target):
super().__init__(runner_cmd=['true'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per earlier review round, runner_cmd does not belong to TargetRunner, so it needs to be removed from there and a subclass created that does handle a running process (e.g. let's call it SubprocessTargetRunner). NOPTargetRunner will not inherit from SubprocessTargetRunner, it will inherit straight from TargetRunner.

In other words, having to execute a command is a pure implementation detail of a specific runner. It happens that a few of them do require that, but it should by no mean be part of the "public interface" of TargetRunner in general, as made obvious by NOPTargetRunner. That public interface is essentially limited to providing __enter__/__exit__, a .target attribute and a couple ancillary methods like terminate() and wait_boot_completed()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 1 to 6
target-configs:
AndroidTarget:
entry-0:
timeout: 60
connection_settings:
device: 'emulator-5554'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This structure still can't be built easily with simple templating. What we need is

target-configs:
	entry-0:
	  AndroidTarget:
		  timeout: 60
		  connection_settings:
			device: 'emulator-5554'

	entry-1:
	  ...

Concretely we want to be able to easily build a config file using e.g. jinja2 for loops. As it stands, you'd have to make jinja parse each config file you would be trying to merge, which is not possible. With the structure I'm recommending, this can be dealt by just including the content from multiple "snippet" files (and maybe use a filter to increase indentation level).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@pytest.mark.parametrize("target, target_runner", build_targets())
def test_read_multiline_values(target, target_runner):
# pylint: disable=redefined-outer-name
def test_read_multiline_values(build_target_runners):
Copy link
Contributor

@douglas-raillard-arm douglas-raillard-arm May 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you confirm that if another test is defined, build_target_runners() is not called again and the existing runners re-used ? I'd expect it considered that fixture has "module" scope but let's double check.

EDIT: The doc seems to indicate the behavior we want as it states the fixture return value is cached for the scope:

If a requested fixture was executed once for every time it was requested during a test, then this test would fail because both append_first and test_string_only would see order as an empty list (i.e. []), but since the return value of order was cached (along with any side effects executing it may have had) after the first time it was called, both the test and append_first were referencing the same object, and the test saw the effect append_first had on that object.

https://docs.pytest.org/en/7.1.x/how-to/fixtures.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did confirm already. Added Initializing/Destroying resources... logs mainly for that purpose.

PR#667: ARM-software#667

- Implement a test module initializer with a tear down method in
  test/test_target.py
- Make various cleanups in test/test_target.py
- Improve structure of test/test_config.yml (previously
  target_configs.yaml)
- Make docstrings Sphinx compatible
- Make ``TargetRunner`` and its subclasses private
- Cleanup tests/test_target.py
- Replace print()'s with appropriate logging calls
- Implement ``NOPTargetRunner`` class for simplifying tests
- Improve Python v3.7 compatibility
- Relax host machine type checking
- Escape user input strings

and more..

Signed-off-by: Metin Kaya <metin.kaya@arm.com>
@metin-arm
Copy link
Contributor Author

Only updated tools/docker/target_configs.yml in the last push (384c2aefa47).

@douglas-raillard-arm
Copy link
Contributor

LGTM

@marcbonnici marcbonnici merged commit 492d42d into ARM-software:master May 29, 2024
@metin-arm metin-arm deleted the pr-667 branch June 27, 2024 07:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants