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

Restore time_sync module on macOS #75

Merged
merged 11 commits into from
Sep 13, 2023
Merged

Conversation

ludwigschwardt
Copy link
Contributor

@ludwigschwardt ludwigschwardt commented Sep 4, 2023

MacOS does not have the adjtimex system call. As a result we have stripped out the entire time_sync module on non-Linux platforms.

However, packages like katsdpcontroller need the ClockState enum without the rest. Instead of spreading the complications, restore the time_sync module with a fake adjtimex instead.

It returns the correct time but with artificially large errors in the absence of real values (and the status is always "OK").

[This was a leftover from the beginning of the year when I wanted to test katsdpcontroller on my laptop. Nearly forgot about it - it was lurking in my working copy for months 🙂 ]

MacOS does not have the `adjtimex` system call. As a result we have
stripped out the entire `time_sync` module on non-Linux platforms.

However, packages like katsdpcontroller need the `ClockState` enum
without the rest. Instead of spreading the complications, restore
the `time_sync` module with a fake `adjtimex` instead. It returns
the correct time but with artificially large errors in the absence
of real values (and the status is always "OK").
Copy link
Contributor

@bmerry bmerry left a comment

Choose a reason for hiding this comment

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

I'm not wild about this approach, because it means the sensors will assert things that are just made up, rather than reporting that the values can't be measured. The only meaningful thing it returns is the current time, and that's not even reported as a sensor value. My preference would be

  • the dummy adjtimex() function raises NotImplementedError
  • TimeSyncUpdater catches NotImplementedError and responds by setting all the sensors to inactive.

Only a couple of tests would need to be disabled on MacOS. If you also added a test that mocked in the NotImplemented implementation and checked the response, you could get test coverage of that fallback on every OS.

src/aiokatcp/adjtimex.py Show resolved Hide resolved
adjtimex = _libc.adjtimex # noqa: F811
adjtimex.argtypes = [ctypes.POINTER(Timex)] # type: ignore[attr-defined]
adjtimex.restype = ctypes.c_int # type: ignore[attr-defined]
adjtimex.errcheck = _errcheck # type: ignore[attr-defined]
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really like this approach of replacing a function, and clearly mypy doesn't either. I'd rather have _adjtimex_dummy and _adjtimex_real functions (or pick better names) and then either assign one of them to a adjtimex variable, or have an adjtimex function that dispatches to the right one (I'd favour the former just for efficiency).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. I like the Bruce-mypy tag-team clothesline 🤼‍♂️ I wanted to generate an AI picture of the event but Dall-E failed me 😂

Copy link
Contributor Author

@ludwigschwardt ludwigschwardt Sep 5, 2023

Choose a reason for hiding this comment

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

To be fair: mypy didn't like the assignment to attributes, which apparently did not bother it before (there was only one type: ignore on the last line, but now they all complained). It was actually flake8 with the issue [Redefinition of unused name from line n].

.github/workflows/test.yml Show resolved Hide resolved
Instead of kludging adjtimex to produce fake data on non-Linux systems,
rather raise `NotImplementedError` and set the relevant sensors to an
inactive state in `TimeSyncUpdater`. Test both cases in the unit tests.
@ludwigschwardt
Copy link
Contributor Author

Thanks! I've pushed a new version incorporating your suggestions.

I can't figure out the failing tests though. It fails on py311 on both Linux and macOS, and it seems to be unrelated to my fixes. In fact, if I rerun the tests on an older commit that previously passed, it now fails there too 🤔 It also doesn't fail on my laptop. Rerunning these tests several times doesn't help, so it's not just a random fluke.

It's complaining about a closed event loop in TestUnclosedClient.

@bmerry
Copy link
Contributor

bmerry commented Sep 11, 2023

Thanks! I've pushed a new version incorporating your suggestions.

I can't figure out the failing tests though. It fails on py311 on both Linux and macOS, and it seems to be unrelated to my fixes. In fact, if I rerun the tests on an older commit that previously passed, it now fails there too 🤔 It also doesn't fail on my laptop. Rerunning these tests several times doesn't help, so it's not just a random fluke.

It's complaining about a closed event loop in TestUnclosedClient.

The Python 3.11 failures I think are caused by python/cpython#107650, which got backported to 3.11. We'll need to investigate to see if this is a regression in Python or something that we should be handling better.

@bmerry
Copy link
Contributor

bmerry commented Sep 11, 2023

The Python 3.11 failures I think are caused by python/cpython#107650, which got backported to 3.11. We'll need to investigate to see if this is a regression in Python or something that we should be handling better.

I don't think that should hold up this PR, but we should investigate it before making a new release.

@bmerry
Copy link
Contributor

bmerry commented Sep 11, 2023

I've opened NGC-1089 for the Python 3.11 issue.

src/aiokatcp/adjtimex.py Outdated Show resolved Hide resolved
tests/test_time_sync.py Outdated Show resolved Hide resolved
tests/test_time_sync.py Outdated Show resolved Hide resolved
tests/test_time_sync.py Outdated Show resolved Hide resolved
("expected_status",),
[
pytest.param(Sensor.Status.NOMINAL, marks=_linux),
pytest.param(Sensor.Status.INACTIVE, marks=_non_linux),
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than (or perhaps in addition to) doing this test only on non-Linux, I'd suggest doing it on all OSes by using mocking to replace adjtimex by _no_adjtimex, which allows the NotImplementedError handling to be tested on any OS.

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'm not getting away here :-) Done!

.github/workflows/test.yml Show resolved Hide resolved
@bmerry
Copy link
Contributor

bmerry commented Sep 11, 2023

Also don't forget to bump the year in the copyright notice.

@ludwigschwardt ludwigschwardt force-pushed the restore-time-sync-on-macos branch 2 times, most recently from f9f35d6 to 0fa9e13 Compare September 11, 2023 15:01
@coveralls
Copy link

coveralls commented Sep 11, 2023

Coverage Status

coverage: 93.408% (-0.1%) from 93.512% when pulling 120544c on restore-time-sync-on-macos into b59b113 on master.

This appeases mypy and removes some superfluous bits.

Thanks @bmerry for the suggestions.

Co-authored-by: Bruce Merry <1963944+bmerry@users.noreply.github.com>
@ludwigschwardt
Copy link
Contributor Author

OK, after many force-pushes I'm ready for another round 😅

.gitignore Outdated
@@ -7,3 +7,6 @@
src/aiokatcp.egg-info
*.pyc
__pycache__

# macOS detritus
.DS_Store
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like that should go into your global core.excludesfile rather than here, because it's specific to your development environment rather than specific to the repository.

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, I did not know about that file... I merely cargo-culted some other repo's ignores (katpoint, etc). Much better idea!

def test_smoke(
sensors: SensorSet, updater: TimeSyncUpdater, expected_status: Sensor.Status
) -> None:
"""Test with real adjtimex on Linux to ensure it interacts cleanly with kernel."""
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure that docstring is accurate. It only covers one of the two parametrisations.

Come to think of it, I'm not sure parametrisation is really necessary, since they're a partition. You could just write

expected_status = Sensor.Status.NOMINAL if sys.platform == "linux" else Sensor.Status.INACTIVE

inside the test function.

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, I missed the partition. I originally had tests for linux and darwin until I merged them.

assert isinstance(sensors["ntp.state"].value, ClockState)
for sensor in sensors.values():
# Check that it actually got updated
assert sensor.status == Sensor.Status.INACTIVE
Copy link
Contributor

Choose a reason for hiding this comment

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

A lot of code duplication here. I'd suggest either:

  • Parametrise test_smoke with a boolean that says whether to mock, and conditionally use mocker inside the test; or
  • Factor out a helper function that does all the work of the test, and have the test functions call it.

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've gone for option 1 and lost 20 lines.

Ignores specific to the user environment is best put in the global
`core.excludesfile`.
Optionally pretend that adjtimex is not there, and set the expected
sensor status based on that and whether we are on Linux. No need for
skipping tests anymore, since `test_smoke` will always run in some
shape or form. This consolidates the smoke tests as well.

Improve the documentation.
@ludwigschwardt
Copy link
Contributor Author

Thanks for the suggestions! You can check again.

Copy link
Contributor

@bmerry bmerry left a comment

Choose a reason for hiding this comment

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

LGTM. Go ahead and merge - there is a separate ticket for the Python 3.11 failures.

@ludwigschwardt ludwigschwardt merged commit 3984b77 into master Sep 13, 2023
16 of 22 checks passed
@ludwigschwardt ludwigschwardt deleted the restore-time-sync-on-macos branch September 13, 2023 07:41
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