helpers icon indicating copy to clipboard operation
helpers copied to clipboard

Come up with a testing approach for release flow

Open gpsaggese opened this issue 8 months ago • 13 comments

As per https://github.com/causify-ai/helpers/pull/590#pullrequestreview-2792676001 when we improve / refactor critical code like the docker flow we don't have a good way to test it, besides running it in production.

We are going to have soon a way to run tests outside a container, which will make it possible to run tests for this part of the repo. The next problem is how to test without actually running? An idea is to check that, under certain mocking, the sequence of system calls generated is the expected one.

FYI @heanhsok @dremdem @samarth9008

gpsaggese avatar Apr 24 '25 22:04 gpsaggese

@sandeepthalapanane let's make a proposal on how to test it first.

Some related ideas are for https://docs.google.com/document/d/1xPgQ2tWXQuVWKkGVONjOGd5j14mXSmGeY_4d1_sGzAE/edit?tab=t.0#heading=h.1baftm82yypg

gpsaggese avatar Apr 28 '25 20:04 gpsaggese

As per your suggestions above, we could use unittest.mock to test the Docker release workflow without requiring actual Docker operations and to mock external dependencies

Code Structure

  1. Mock Infrastructure

    • Mock Docker client interactions
    • Mock system calls (hsystem.system, hlitauti.run)
    • Mock registry client for AWS ECR and DockerHub
    • Mock environment variables
  2. Test Categories

    a. Command Generation Tests

    • Check that the correct sequence of commands is generated for different scenarios
    • Test multi-architecture build commands
    • Validate registry-specific commands

    b. Error Handling Tests

    • Test unsupported registry scenarios
    • Verify version validation
    • Test system error propagation

sandeepthalapanane avatar Apr 28 '25 22:04 sandeepthalapanane

The plan looks reasonable. I think you understood the issue.

The goal is to ensure that by mocking everything that needs to be mocked, we can get a sequence of commands that is the one expected. After testing end-to-end with the mocks, we can unit test "deterministic" chunks of code by extracting code that doesn't interact and just testing it in isolation.

My proposal is to test a single function, showing how things will work and after that PR start expanding the coverage as we go.

@heanhsok, any thoughts?

gpsaggese avatar Apr 28 '25 22:04 gpsaggese

The plan looks reasonable. I think you understood the issue.

The goal is to ensure that by mocking everything that needs to be mocked, we can get a sequence of commands that is the one expected. After testing end-to-end with the mocks, we can unit test "deterministic" chunks of code by extracting code that doesn't interact and just testing it in isolation.

My proposal is to test a single function, showing how things will work and after that PR start expanding the coverage as we go.

@heanhsok, any thoughts?

I think I will start by testing the docker_build_local_image (both single arch and multi-arch) since it's the first step in the docker release flow and covers many things.

sandeepthalapanane avatar Apr 28 '25 22:04 sandeepthalapanane

I think I will start by testing the docker_build_local_image (both single arch and multi-arch) since it's the first step in the docker release flow and covers many things.

I don't have anything further to add right now, but this plan sounds good.

heanhsok avatar Apr 28 '25 23:04 heanhsok

Found one interesting Docker feature: https://docs.docker.com/build/checks/ https://www.docker.com/blog/introducing-docker-build-checks/

Check a build without building To run build checks without actually building, you can use the docker build command as you typically would, but with the addition of the --check flag. Here's an example: docker build --check . Instead of executing the build steps, this command only runs the checks and reports any issues it finds. If there are any issues, they will be reported in the output.

It might be worth adding this to our tests and even including it in the Linter checks for Dockerfiles. WDYT @samarth9008 ?

dremdem avatar May 05 '25 08:05 dremdem

@dremdem

Can you figure out other who are involved in the issue? Could be GP or Heanh

samarth9008 avatar May 05 '25 12:05 samarth9008

https://github.com/causify-ai/helpers/issues/615#issuecomment-2850310356

@heanhsok Can you take a look pls.

dremdem avatar May 05 '25 13:05 dremdem

  • [x] PR comments https://github.com/causify-ai/helpers/pull/733/files
  • [x] @heanhsok to review architecture / mocking approach

gpsaggese avatar May 19 '25 19:05 gpsaggese

Good test implementation overall

Nits

Just one small nit (apart from the unresolved comments in the PR https://github.com/causify-ai/helpers/pull/733/files). Can we make the tests directory-agnostic (i.e., so the tests can be run from any directory)?

For example, running the tests from a non-root directory currently causes errors. We can use the with hsystem.cd(..) to temporarily change the context.

user_1042@4dfc7fb1ac6f:/app/helpers/test$ pytest test_lib_tasks_docker_release.py::Test_docker_build_local_image1::test_multi_arch1
...
test_lib_tasks_docker_release.py::Test_docker_build_local_image1::test_multi_arch1 (0.00 s) FAILED                                                         [100%]
======================================================================= FAILURES ========================================================================
____________________________________________________ Test_docker_build_local_image1.test_multi_arch1 ____________________________________________________
Traceback (most recent call last):
  File "/usr/lib/python3.12/unittest/case.py", line 58, in testPartExecutor
    yield
  File "/usr/lib/python3.12/unittest/case.py", line 634, in run
    self._callTestMethod(testMethod)
  File "/usr/lib/python3.12/unittest/case.py", line 589, in _callTestMethod
    if method() is not None:
       ^^^^^^^^
  File "/app/helpers/test/test_lib_tasks_docker_release.py", line 200, in test_multi_arch1
    hltadore.docker_build_local_image(
  File "/venv/lib/python3.12/site-packages/invoke/tasks.py", line 138, in __call__
    result = self.body(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/helpers/lib_tasks_docker_release.py", line 384, in docker_build_local_image
    _prepare_docker_ignore(ctx, docker_ignore)
  File "/app/helpers/lib_tasks_docker_release.py", line 51, in _prepare_docker_ignore
    hdbg.dassert_path_exists(docker_ignore)
  File "/app/helpers/hdbg.py", line 756, in dassert_path_exists
    _dfatal(txt, msg, *args, only_warning=only_warning)
  File "/app/helpers/hdbg.py", line 142, in _dfatal
    dfatal(dfatal_txt)
  File "/app/helpers/hdbg.py", line 71, in dfatal
    raise assertion_type(ret)
AssertionError:
################################################################################
* Failed assertion *
Path '/app/helpers/test/devops/docker_build/dockerignore.dev' doesn't exist!
################################################################################

================================================================== slowest 3 durations ==================================================================
0.01s call     helpers/test/test_lib_tasks_docker_release.py::Test_docker_build_local_image1::test_multi_arch1
0.00s setup    helpers/test/test_lib_tasks_docker_release.py::Test_docker_build_local_image1::test_multi_arch1
0.00s teardown helpers/test/test_lib_tasks_docker_release.py::Test_docker_build_local_image1::test_multi_arch1
================================================================ short test summary info ================================================================
FAILED test_lib_tasks_docker_release.py::Test_docker_build_local_image1::test_multi_arch1 - AssertionError:
################################################################################
* Failed assertion *
Path '/app/helpers/test/devops/docker_build/dockerignore.dev' doesn't exist!
################################################################################

Some further improvements

  • The tests primarily cover happy paths. Including edge cases and error scenarios could be beneficial, as it would improve the coverage
  • Do a dry run to check that the commands being executed are valid and won't cause errors at runtime (perhaps the Docker build checks feature can help with that)

WDYT? @gpsaggese @dremdem

Feel free to address just the Nits first @sandeepthalapanane

heanhsok avatar May 27 '25 21:05 heanhsok

Good test implementation overall

Nits

Just one small nit (apart from the unresolved comments in the PR https://github.com/causify-ai/helpers/pull/733/files). Can we make the tests directory-agnostic (i.e., so the tests can be run from any directory)?

For example, running the tests from a non-root directory currently causes errors. We can use the with hsystem.cd(..) to temporarily change the context.

user_1042@4dfc7fb1ac6f:/app/helpers/test$ pytest test_lib_tasks_docker_release.py::Test_docker_build_local_image1::test_multi_arch1
...
test_lib_tasks_docker_release.py::Test_docker_build_local_image1::test_multi_arch1 (0.00 s) FAILED                                                         [100%]
======================================================================= FAILURES ========================================================================
____________________________________________________ Test_docker_build_local_image1.test_multi_arch1 ____________________________________________________
Traceback (most recent call last):
  File "/usr/lib/python3.12/unittest/case.py", line 58, in testPartExecutor
    yield
  File "/usr/lib/python3.12/unittest/case.py", line 634, in run
    self._callTestMethod(testMethod)
  File "/usr/lib/python3.12/unittest/case.py", line 589, in _callTestMethod
    if method() is not None:
       ^^^^^^^^
  File "/app/helpers/test/test_lib_tasks_docker_release.py", line 200, in test_multi_arch1
    hltadore.docker_build_local_image(
  File "/venv/lib/python3.12/site-packages/invoke/tasks.py", line 138, in __call__
    result = self.body(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/helpers/lib_tasks_docker_release.py", line 384, in docker_build_local_image
    _prepare_docker_ignore(ctx, docker_ignore)
  File "/app/helpers/lib_tasks_docker_release.py", line 51, in _prepare_docker_ignore
    hdbg.dassert_path_exists(docker_ignore)
  File "/app/helpers/hdbg.py", line 756, in dassert_path_exists
    _dfatal(txt, msg, *args, only_warning=only_warning)
  File "/app/helpers/hdbg.py", line 142, in _dfatal
    dfatal(dfatal_txt)
  File "/app/helpers/hdbg.py", line 71, in dfatal
    raise assertion_type(ret)
AssertionError:
################################################################################
* Failed assertion *
Path '/app/helpers/test/devops/docker_build/dockerignore.dev' doesn't exist!
################################################################################

================================================================== slowest 3 durations ==================================================================
0.01s call     helpers/test/test_lib_tasks_docker_release.py::Test_docker_build_local_image1::test_multi_arch1
0.00s setup    helpers/test/test_lib_tasks_docker_release.py::Test_docker_build_local_image1::test_multi_arch1
0.00s teardown helpers/test/test_lib_tasks_docker_release.py::Test_docker_build_local_image1::test_multi_arch1
================================================================ short test summary info ================================================================
FAILED test_lib_tasks_docker_release.py::Test_docker_build_local_image1::test_multi_arch1 - AssertionError:
################################################################################
* Failed assertion *
Path '/app/helpers/test/devops/docker_build/dockerignore.dev' doesn't exist!
################################################################################

It's not the tests that are failing but the hltadore.docker_build_local_image() function is failing. Do you want me to change the directory for every test before running the tested function?

Some further improvements

  • The tests primarily cover happy paths. Including edge cases and error scenarios could be beneficial, as it would improve the coverage

I did check the coverage and covered as many as I could with mocking in the final pull request. I will take a look to see if I have missed any error scenarios. The coverage is around 80-85%, and I think the coverage report had the lines of code that I was not testing. I checked line by line and made sure to include and omit if it's not possible to mock or test.

  • Do a dry run to check that the commands being executed are valid and won't cause errors at runtime (perhaps the Docker build checks feature can help with that)

I did take a look at Docker build checks; it's currently in the development stage, not completely ready. The features are limited, and probably, I will take a look again and see if we could implement it.

sandeepthalapanane avatar May 28 '25 06:05 sandeepthalapanane

Can we make the tests directory-agnostic (i.e., so the tests can be run from any directory)?

Not sure it's worth doing right now.

I checked all the fast tests:

  • In the test directory (/app/helpers/test):
    • 24 failed, 1007 passed, 103 skipped, 54 deselected, 1 rerun in 59.02s
  • In the root directory (/app):
    • 1444 passed, 182 skipped, 91 deselected in 87.03s

It seems like some of our tests are not intended to run outside the root directory.

Do a dry run to check that the commands being executed are valid and won't cause errors at runtime (perhaps the Docker build checks feature can help with that)

Nice idea. I filed the similar issue: https://github.com/causify-ai/helpers/issues/673

dremdem avatar May 28 '25 07:05 dremdem

It seems like some of our tests are not intended to run outside the root directory.

Maybe something like this will solve that and make things more explicit. WDYT?

git_root_dir = hgit.find_git_root()
with hsystem.cd(git_root_dir):
    hltadore.docker_build_local_image(
        self.mock_ctx,
        self.test_version,
        cache=False,
        base_image=self.test_base_image,
        poetry_mode="update",
        multi_arch=self.test_multi_arch,
    )

heanhsok avatar May 29 '25 15:05 heanhsok

It seems like some of our tests are not intended to run outside the root directory.

Maybe something like this will solve that and make things more explicit. WDYT?

git_root_dir = hgit.find_git_root() with hsystem.cd(git_root_dir): hltadore.docker_build_local_image( self.mock_ctx, self.test_version, cache=False, base_image=self.test_base_image, poetry_mode="update", multi_arch=self.test_multi_arch, )

@dremdem could you check on this, so that we could either make changes or close this issue as completed.

sandeepthalapanane avatar Jun 03 '25 08:06 sandeepthalapanane

close this issue as completed.

Yes, let's close it.

dremdem avatar Jun 03 '25 13:06 dremdem

Merged to master https://github.com/causify-ai/helpers/pull/770, https://github.com/causify-ai/helpers/pull/707, https://github.com/causify-ai/helpers/pull/733, https://github.com/causify-ai/helpers/pull/640, and https://github.com/causify-ai/helpers/pull/651.

sandeepthalapanane avatar Jun 15 '25 01:06 sandeepthalapanane

Done

sandeepthalapanane avatar Jun 15 '25 01:06 sandeepthalapanane