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

Refactor configuration files and tests #31

Merged
merged 12 commits into from
Apr 18, 2021
Merged

Refactor configuration files and tests #31

merged 12 commits into from
Apr 18, 2021

Conversation

br3ndonland
Copy link
Owner

Description

The Gunicorn and logging configuration files were technically covered by unit tests, but the individual settings were not tested. This PR will add a dedicated test module for each configuration file, and add tests to verify the Gunicorn and logging settings.

One of the major benefits of unit testing is that the tests not only verify that code is working as expected, but also reveal where the code is not working as expected. This was the case here, and these updates result in cleaner and more correct source code.

Changes

  • Move Gunicorn conf tests to separate module (d28753b)
  • Refactor Gunicorn worker calculation and tests (315c1c1)
    • The gunicorn_conf.calculate_workers() method returns a desired number of Gunicorn workers, based on the arguments passed in. The arguments are read from environment variables.
    • The calculation is deceptively complex, and a variety of edge cases emerged over time. The tests were correspondingly complex and convoluted, testing various interrelated conditions, and becoming less and less readable.
    • This commit will refactor the Gunicorn worker calculation and tests. The result is more readable (and correct) code.
  • Organize Gunicorn settings (aaef2f0): The same result is obtained with less than half the lines of code (29 -> 14).
  • Test Gunicorn settings (324f88f)
    • Start Gunicorn with the --print-config setting to load and output the configuration.
    • Use the capfd pytest fixture to read the Gunicorn output.
    • These could be considered integration tests, because Gunicorn is actually starting and loading the settings. It also means they are substantially slower than the other tests, but the entire test suite still runs in only a few seconds.
  • Use similar syntax to set up Gunicorn and Uvicorn (1b739ed)
  • Consolidate logging configuration method and tests (8cd8db2)
    • The configure_logging() method was previously located in start.py, but the logging configuration dictionary was in logging_conf.py. This organization was not ideal for readability and separation of concerns. It also created logistical complications, such as requiring the Gunicorn configuration file gunicorn_conf.py to import from start.py, so that it could configure logging for Gunicorn. The start.py script should be self-contained, and should import other modules and objects from the package, not the other way around.
    • This commit will move configure_logging() to logging_conf.py, and move the tests to a corresponding module, test_logging_conf.py. This matches up nicely with the latest updates to the gunicorn_conf.py, by having a setup method at the top of the module, and the settings below.
  • Correct attribute error on module spec loader (0ba94fc)
  • Split up logging configuration method (373c98d)
    • The configure_logging() method did multiple things: find and import a logging configuration module, load the logging configuration dictionary, and apply the dictionary configuration to the Python logger.
    • This commit will refactor configure_logging() into two methods: find_and_load_logging_conf() will return a dictionary configuration, and configure_logging() will apply the dictionary configuration.
    • The return value of configure_logging() will remain the same, so there will not be any changes to the programming API.
    • Also, now that the configure_logging() method is in the same module as the logging configuration dictionary, that dictionary will be used by default, instead of finding the module separately.
  • Remove prefix from mocker objects in logger tests (9a61fc2)
  • Test logger output after configuring logging (273f795)
    • In addition to testing the methods that load the logging configuration, it can also be useful to test that the logger messages have the expected format. This commit will add tests that output logger messages in various formats and assert that each output matches the expected format.
    • As in the tests of the Gunicorn server output, these tests will use the capfd pytest fixture.
    • It is also possible to use the caplog fixture, but I have not had as much success with it.
  • Update links to pytest docs (aa243dd): Love the new docs!
  • Clarify Gunicorn variable descriptions in README (40baca4)

Related

#3
#11

#11

These tests weren't really testing the start script, so they shouldn't
be in test_start.py. Moving the tests to test_gunicorn_conf.py also
helps emulate the directory structure of the source code, so tests are
more intuitively found.
#11
02b249e
fd60470
https://docs.python.org/3/reference/expressions.html#boolean-operations

The `gunicorn_conf.calculate_workers()` method returns a desired number
of Gunicorn workers, based on the arguments passed in. The arguments are
read from environment variables. The calculation is deceptively complex,
and a variety of edge cases emerged over time. Commit fd60470 fixed the
web concurrency calculation, but not the max workers calculation. There
should have been an `else use_max_workers if max_workers_str` condition.
The tests were correspondingly complex and convoluted, testing various
interrelated conditions, and becoming less and less readable.

This commit will refactor the Gunicorn worker calculation and tests. The
result is more readable (and correct) code.

Refactor the `gunicorn_conf.calculate_workers()` method arguments:

- Remove `_str` from function arguments: arguments are type-annotated,
  and it is redundant to add `_str` to arguments annotated as strings.
- Rename the `web_concurrency_str` function argument to `total_workers`:
  the "web concurrency" name is a legacy Gunicorn environment variable,
  and doesn't really convey what the setting does. "Web concurrency" is
  a total number of workers, so just call it `total_workers`.
- Make the `workers_per_core` argument a keyword argument (kwarg): the
  corresponding environment variable `WORKERS_PER_CORE` has a default,
  so set the same default on the kwarg.
- Move the cpu cores calculation into the function body: not necessary
  to set a number of cores, so just use `multiprocessing.cpu_count()`.
- Keep same order of arguments and argument type-annotations to avoid
  breaking existing functionality and to ensure backwards-compatibility.

Refactor the `gunicorn_conf.calculate_workers()` method logic, by using
`if` expressions to more clearly evaluate each condition, then returning
the correct value by using `or` to evaluate Boolean conditions in order.

Improve test parametrization to more thoroughly test use cases, and
refactor tests so that each one tests an isolated use case:

- Check defaults
- Set maximum number of workers
- Set total number of workers ("web concurrency")
- Set desired number of workers per core
- Set both maximum number of workers and total number of workers
4914a4b

- Remove unnecessary setup section: just read environment variables when
  instantiating the settings objects that are directly read by Gunicorn.
  The same result is obtained with half the lines of code (29 -> 14).
- Sort settings alphabetically
#11
https://docs.gunicorn.org/en/latest/settings.html#print-config
https://docs.pytest.org/en/latest/how-to/capture-stdout-stderr.html

The Gunicorn configuration file was technically covered by unit tests,
but the individual settings were not tested. This commit will add tests
of the Gunicorn settings. The tests use Gunicorn's `--print-config`
option to load and output the configuration. The pytest fixture `capfd`
is used to read the Gunicorn output.
#30

This commit will add some minor improvements to the refactor of the
Gunicorn and Uvicorn setup in start.py (added in #30).

- Pass `app_module` into `start.set_gunicorn_options()`, rather than
  appending later when running the server
- Read environment variables earlier in `start.set_uvicorn_options()` to
  provide consistent syntax when returning the dictionary at the end:
  this strategy seems opposed to the one taken in `gunicorn_conf.py`
  (read environment variables when they are used, rather than before),
  but the end result is similar (consistent syntax in the settings).
#3

The `configure_logging()` method was previously located in `start.py`,
but the logging configuration dictionary was in `logging_conf.py`. This
organization was not ideal for readability and separation of concerns.
It also created logistical complications, such as requiring the Gunicorn
configuration file `gunicorn_conf.py` to import from start.py, so that
it could configure logging for Gunicorn. The `start.py` script should be
self-contained, and should import other modules and objects from the
package, not the other way around.

This commit will move `configure_logging()` to `logging_conf.py`, and
move the tests to a corresponding module, `test_logging_conf.py`. This
matches up nicely with the latest updates to the `gunicorn_conf.py`, by
having a setup method at the top of the module, and the settings below.
The loader attribute on a module spec is technically optional, so mypy
raises a `[union-attr]` error:

Item "_Loader" of "Optional[_Loader]" has no attribute "exec_module"

To avoid referencing the potentially undefined `_Loader.exec_module`
attribute, this commit will update the syntax to get the attribute with
`getattr(spec.loader, "exec_module")` prior to running `exec_module()`.
An `AttributeError` will be raised if the attribute does not exist.
#3

The `configure_logging()` method did multiple things: find and import a
logging configuration module, load the logging configuration dictionary,
and apply the dictionary configuration to the Python logger.

This commit will refactor `configure_logging()` into two methods.

1. `find_and_load_logging_conf()` will return a dictionary configuration
2. `configure_logging()` will apply the dictionary configuration

The return value of `configure_logging()` will remain the same, so there
will not be any changes to the programming API.

Also, now that the `configure_logging()` method is in the same module as
the logging configuration dictionary, that dictionary will be used by
default, instead of finding the module separately.
Mocker objects are sometimes prefixed with `mock_` to indicate that they
are separate objects. This is largely unnecessary, so it will be removed
from the tests of the logging configuration.
#3
https://docs.pytest.org/en/latest/how-to/capture-stdout-stderr.html
https://docs.pytest.org/en/latest/how-to/logging.html

In addition to testing the methods that load the logging configuration,
it can also be useful to test that logger messages have the expected
format. This commit will add tests that output logger messages in
various formats and assert that each output matches the expected format.

As in the tests of the Gunicorn server output, these tests will use the
pytest `capfd` fixture. It is also possible to use the `caplog` fixture,
but I have not had as much success with it.
- `MAX_WORKERS`
- `WEB_CONCURRENCY`
- `WORKERS_PER_CORE`
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Apr 18, 2021

Sourcery Code Quality Report

✅  Merging this PR will increase code quality in the affected files by 0.81%.

Quality metrics Before After Change
Complexity 1.25 ⭐ 1.16 ⭐ -0.09 👍
Method Length 63.92 🙂 62.95 🙂 -0.97 👍
Working memory 9.45 🙂 9.26 🙂 -0.19 👍
Quality 72.48% 🙂 73.29% 🙂 0.81% 👍
Other metrics Before After Change
Lines 917 715 -202
Changed files Quality Before Quality After Quality Change
inboard/gunicorn_conf.py 57.89% 🙂 70.75% 🙂 12.86% 👍
inboard/logging_conf.py % 70.90% 🙂 %
inboard/start.py 68.61% 🙂 74.03% 🙂 5.42% 👍
tests/test_start.py 72.64% 🙂 69.65% 🙂 -2.99% 👎
tests/app/test_main.py 78.44% ⭐ 78.44% ⭐ 0.00%

Here are some functions in these files that still need a tune-up:

File Function Complexity Length Working Memory Quality Recommendation
tests/test_start.py TestSetAppModule.TestStartServer.test_start_server_uvicorn_reload_dirs 2 ⭐ 167 😞 16 ⛔ 50.22% 🙂 Try splitting into smaller methods. Extract out complex expressions
tests/test_start.py TestSetAppModule.TestStartServer.test_start_server_uvicorn_gunicorn_custom_config 0 ⭐ 143 😞 13 😞 58.48% 🙂 Try splitting into smaller methods. Extract out complex expressions
tests/test_start.py TestSetAppModule.TestStartServer.test_start_server_uvicorn_gunicorn 0 ⭐ 117 🙂 14 😞 60.15% 🙂 Extract out complex expressions
tests/app/test_main.py TestEndpoints.test_get_status_message 4 ⭐ 103 🙂 12 😞 60.98% 🙂 Extract out complex expressions
tests/test_start.py TestSetAppModule.TestStartServer.test_start_server_uvicorn 0 ⭐ 109 🙂 12 😞 64.16% 🙂 Extract out complex expressions

Legend and Explanation

The emojis denote the absolute quality of the code:

  • ⭐ excellent
  • 🙂 good
  • 😞 poor
  • ⛔ very poor

The 👍 and 👎 indicate whether the quality has improved or gotten worse with this pull request.


Please see our documentation here for details on how these metrics are calculated.

We are actively working on this report - lots more documentation and extra metrics to come!

Let us know what you think of it by mentioning @sourcery-ai in a comment.

@codecov
Copy link

codecov bot commented Apr 18, 2021

Codecov Report

❗ No coverage uploaded for pull request base (develop@35d6772). Click here to learn what that means.
The diff coverage is 100.00%.

Impacted file tree graph

@@             Coverage Diff             @@
##             develop       #31   +/-   ##
===========================================
  Coverage           ?   100.00%           
===========================================
  Files              ?         9           
  Lines              ?       256           
  Branches           ?         0           
===========================================
  Hits               ?       256           
  Misses             ?         0           
  Partials           ?         0           
Flag Coverage Δ
unittests 100.00% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
inboard/gunicorn_conf.py 100.00% <100.00%> (ø)
inboard/logging_conf.py 100.00% <100.00%> (ø)
inboard/start.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 35d6772...40baca4. Read the comment docs.

@br3ndonland br3ndonland merged commit c9dd974 into develop Apr 18, 2021
@br3ndonland br3ndonland deleted the conf-testing branch April 18, 2021 18:54
@br3ndonland br3ndonland mentioned this pull request Apr 18, 2021
1 task
br3ndonland added a commit that referenced this pull request May 2, 2021
#31
315c1c1

Commit 315c1c1 improved the calculation of the number of Gunicorn worker
processes started by the inboard web server.

The `gunicorn_conf.calculate_workers()` method was update to return
`use_least or use_max or use_total or use_default`. However, this meant
that `use_max`, which is set by the `MAX_WORKERS` environment variable,
could potentially be used as the total number of workers, when it should
only be used as a maximum, not as a total.

This commit will update `gunicorn_conf.calculate_workers()` and related
tests so that `MAX_WORKERS` is only used to set a limit on the total,
not to set the total itself. The `gunicorn_conf.calculate_workers()`
method will now return `use_least or use_total or use_default`.
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.

1 participant