Skip to content

Settings primary name#805

Merged
bors[bot] merged 64 commits intomasterfrom
settings-primary-name
Jun 4, 2019
Merged

Settings primary name#805
bors[bot] merged 64 commits intomasterfrom
settings-primary-name

Conversation

@ricab
Copy link
Collaborator

@ricab ricab commented May 28, 2019

This fixes #766

@ricab ricab added the no-merge label May 28, 2019
@ricab
Copy link
Collaborator Author

ricab commented May 28, 2019

Sys-tray GUI side still pending

@ricab
Copy link
Collaborator Author

ricab commented May 28, 2019

Note: although settings have a hierarchical appearance (i.e. their keys are structured with dots like client.primary_name), they are currently saved to disk in INI format (QSettings default). QSettings does not offer a YAML backend, but we should be able to register one.

@ricab ricab removed the no-merge label May 28, 2019
@ricab
Copy link
Collaborator Author

ricab commented May 28, 2019

This is ready to merge, as per discussion in IRC:

  1. the petenv does not intersect with the systray yet, so the file-watcher bit is postponed until then (when the systray gets top-level commands).
  2. I am creating a separate issue for saving settings in YAML format

@ricab
Copy link
Collaborator Author

ricab commented May 28, 2019

This is ready to merge

Travis disagrees, looking into it.

@ricab
Copy link
Collaborator Author

ricab commented May 28, 2019

It should be good now...

@codecov
Copy link

codecov bot commented May 28, 2019

Codecov Report

Merging #805 into master will increase coverage by 0.06%.
The diff coverage is 75.23%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #805      +/-   ##
==========================================
+ Coverage   68.59%   68.66%   +0.06%     
==========================================
  Files         179      188       +9     
  Lines        6289     6385      +96     
==========================================
+ Hits         4314     4384      +70     
- Misses       1975     2001      +26
Impacted Files Coverage Δ
src/client/cli/cmd/shell.h 100% <ø> (ø) ⬆️
src/client/cli/cmd/start.h 100% <ø> (ø) ⬆️
.../multipass/exceptions/invalid_settings_exception.h 0% <0%> (ø)
include/multipass/settings.h 0% <0%> (ø)
src/client/cli/cmd/restart.cpp 91.17% <100%> (+0.26%) ⬆️
src/client/cli/cmd/set.h 100% <100%> (ø)
src/client/cli/cmd/get.h 100% <100%> (ø)
src/client/cli/client.cpp 100% <100%> (ø) ⬆️
src/client/cli/cmd/stop.cpp 93.75% <100%> (+0.13%) ⬆️
src/client/cli/cmd/shell.cpp 77.77% <100%> (+0.41%) ⬆️
... and 17 more

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 b04a7f5...e0504b5. Read the comment docs.

@codecov
Copy link

codecov bot commented May 28, 2019

Codecov Report

Merging #805 into master will decrease coverage by 0.24%.
The diff coverage is 71.05%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master    #805      +/-   ##
=========================================
- Coverage   68.94%   68.7%   -0.25%     
=========================================
  Files         182     190       +8     
  Lines        6401    6508     +107     
=========================================
+ Hits         4413    4471      +58     
- Misses       1988    2037      +49
Impacted Files Coverage Δ
src/client/cli/cmd/shell.h 100% <ø> (ø) ⬆️
src/client/cli/cmd/start.h 100% <ø> (ø) ⬆️
include/multipass/settings.h 0% <0%> (ø)
src/client/cli/cmd/restart.cpp 91.17% <100%> (+0.26%) ⬆️
src/client/cli/cmd/set.h 100% <100%> (ø)
src/client/cli/cmd/get.h 100% <100%> (ø)
src/client/cli/client.cpp 100% <100%> (ø) ⬆️
src/client/cli/cmd/stop.cpp 93.75% <100%> (+0.13%) ⬆️
src/client/cli/cmd/shell.cpp 77.77% <100%> (+0.41%) ⬆️
src/client/cli/cmd/start.cpp 93.54% <100%> (+0.1%) ⬆️
... and 32 more

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 69cfb24...a16be82. Read the comment docs.

Copy link
Contributor

@townsend2010 townsend2010 left a comment

Choose a reason for hiding this comment

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

I've only begun testing this and haven't really looked at the code yet. So first issue I hit is that when I start this version of the daemon, it does not see any already existing instances. Once that is fixed, I'll go from there:)

@ricab
Copy link
Collaborator Author

ricab commented May 29, 2019

oO really ? let me check

@ricab
Copy link
Collaborator Author

ricab commented May 29, 2019

@townsend2010 thanks for catching that, now I understand why I kept downloading images when switching branches :) (thought they were updating a lot)

I have pushed the fix. The issue was that, by setting organization name and domain in QCoreApplication, as advised in QSettings docs, I was also affecting other QStandardPaths. But simply leaving them out would result in an "Unknown Organization" folder for the settings.

Although the organization name can be passed directly to QSettings' ctor (rather than determined centrally) on mac the domain would be used instead, and QSettings does not provide a ctor for that. I ended up deriving a custom filename altogether, based on QStandardPaths::AppConfigLocation, the application name, and the suffix .conf (which QSettings used by default).

Note that this code will still have to change with #776 (the client will need access to both its and the daemon's settings file) and possibly for yaml backend.

@townsend2010
Copy link
Contributor

@ricab,

Cool, thanks. There is also a merge conflict happening now too.

@ricab ricab force-pushed the settings-primary-name branch from 4ec7d74 to e607fbd Compare May 30, 2019 10:51
@ricab
Copy link
Collaborator Author

ricab commented May 30, 2019

@townsend2010 rebased and force-pushed

Copy link
Contributor

@gerboland gerboland left a comment

Choose a reason for hiding this comment

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

Hey, I really like the end result, the static Settings is very nice to use. It's great that it is mockable too.

But the complexity of the implementation does cause me concern. I've not used static polymorphism before. For instance my approach to this would be more dynamically polymorphic (i.e virtuals!):

class SettingsInterface { 
    SettingsInterface& instance() { // return Settings or MockSettings, depending on what we want    }
    virtual QString get(QString key) = 0; ... 
}
class Settings: public SettingsInterface { /* concrete impl using QSettings internally */ }
class MockSettings: public SettingsInterface { /* mockable impl */ }

It's a paradigm I'm more accustomed to, but I am always open to new things.

Question: What is the problem that PrivatePass is solving? Is it that it ensures any Singleton will actually be a singleton, i.e. non-copyable, by creating a compiler error if mis-used?

If so, that pretty cool. But the quantity of boilerplate involved, plus the fact it's hard to test (since there's no good way to consider a compile fail as a test pass), makes me worry the same problem can be solved in a simpler but less powerful way.

Options I can think of are:

  1. going super-simple: just using QSettings everywhere is easy until we want to test, as we'd have to resort to trickery (muck with files), which I suspect will be awkward. So I'm ok with discarding that approach.

  2. The dynamic polymorphism approach I gave above should be do-able IMO, and a bit simpler too. Did you consider it, and why did you dismiss it?

Were there other approaches you considered?

I want to understand why you consider this the best approach, just to be sure that the boiler-plate is worth it.

@ricab
Copy link
Collaborator Author

ricab commented May 30, 2019

@gerboland fair questions, I started typing answers a couple of times but I am not inspired ATM, I'll have to reply later.

Meanwhile I pushed fixes for compilation in buggy compilers ;)

@ricab
Copy link
Collaborator Author

ricab commented May 31, 2019

@gerboland sorry for the long reply. I hope it is clear, but let me know if you would like to chat about this or if there are particular places in the code that I could explain better.

What brought me to this approach was really the combination of:

  • having something globally accessible (to avoid param injection)
  • being able to mock it

As you suggest, let's do away with option 1 (direct QSettings). It would not be mockable and it would also mean implementing extra logic (for checking, separate files (coming for the daemon), custom format (need to move to YAML)) in calling sites.

So we needed a wrapper. Your option 2 (regular interface + inheriting implementation and mock classes), was my departure point. It would normally mean passing SettingsInterface objects as a param whose ultimate type was determined elsewhere. As the config grew, multiple entities would need to access it, so the param could start polluting many signatures. Repeating the dependency would add entropy, but not much information, because we could approximate it by just saying "everyone depends on it" in a single place. I tried to explain this in the doc I wrote.

OK, so what if we just dropped the param but created a new object whenever needed? Well, we could not use the ctor directly if we wanted to mock it, because which type would we use? We would need some sort of factory. This factory would be an interface with an implementation for tests and one for real code. But which one do we call? Well, we'd need to pass one factory implementation in normal execution and another in tests. But for that we'd need to inject a dependency everywhere again. Basically we'd be back to the same problem as before.

What we needed was a way to say "here is the settings object, everyone can use it", in a single place. We'd determine the exact object (and thus its type) only once.

The Settings singleton does that. By default it provides a "real" Settings object: one of its own implementation, who does "the real job" we need. But it allows registering a derivation that overrides things for mocking.

Note that this is totally dependent on the dynamic polymorphism (the usual sort). The Settings class declares get/set as virtual and they are overridden by the mock. Relatively to what you propose, the only difference is that I skipped the pure interface part. That's perhaps not as clean looking, but it avoided another inheritance level and provided a ready default implementation that the mock could delegate to (which it does).

The rest is the implementation of the Singleton. That is where the static polymorphism comes into play, and only so we can have a Singleton base class that ensures it really has a single object. (Note: this restriction is actually a bit relaxed, to "only a single object at any point in time". There can be multiple objects over the course of a program, because I provide a protected reset, but that is really only there for mocking.)

To ensure that only one object is created, the Singleton needs to control ctor access. If its ctor was private, descendants could not be constructed (because they would not be able to call the parent's ctor). If it was protected, descendants would be able to construct as many as they liked, so they wouldn't be singletons.

So we use a different mechanism to restrict ctor calling that does not rely on visibilities at all. We actually declare it public, but give it a token parameter that callers need to be able to produce (here). Descendants then require this same token in their ctor and pass it to the parent ctor. We keep a single such token private in the Singleton and we provide it only in calls made by the Singleton itself. In other words, the constructors of Singleton derivations are only called by the Singleton (and intermediate des/ascendants).

Since the Singleton is the one creating the object, it needs to know its derived type. It gets that in the template parameter. But because we also allow mocking, we allow an additional template for the mock. That is the bit that introduces more template complexity.

The final bit is the PrivatePassProvider. This is the root of the singleton inheritance chain. It is just the class that provides the private token discussed above. We could pretty much paste its contents in the singleton directly, but that would make it even harder to read. Besides, having a separate class honors separation of concerns and modularity, so the PrivatePassProvider can be used for other purposes – I actually had it in my code for totally different purposes; it can be useful in general whenever we want fine control of accessibility that goes beyond what normal private/protected/public visibilities provide.

I suspect part of the "boiler-plate" you mention is due to separation of declaration from definition (even if the implementation is in the header). This is arguably less important today, with =default, constexpr, and default initializers, going the other way. But I think it still makes for a much clearer view. You look at the class and get an immediate overview of the interface without having to care about implementation details. If you want those, they are below, (but they do have to repeat templates and namespaces).

As for testability, it can be tested by attempting to compile something and see if it breaks. I provided examples in test_singleton.cpp and test_private_pass_provider.cpp. It is automated testing that is more difficult. I did that here with ifdefs and cmake. It seemed like too much here, but I can do it if requested. There is also DejaGNU but I am not familiar with it.

An alternative I thought of was a free factory function that would be "premocked" or have a header injected. Both sounded hackish and I was unsure if they were doable (I am not familiar with premock).

Another alternative that occurs to me now would be to provide copies or a static "template" object, rather than a reference to it as in a singleton. We would have to return unique_ptrs or something, to avoid chopping. I did not think of that thought. Nevertheless most of the complexity (for registration/resetting/mocking) would still be there. And having a single object makes it easier to reason about, either if it is a simple proxy to what could otherwise be global functions or if it gets state in the future.

@ricab
Copy link
Collaborator Author

ricab commented May 31, 2019

Note to self: merge master and check if/where the new QString/fmt util can be used. TODO @ricab

ricab added 11 commits June 3, 2019 19:00
Tests currently failing.
Cosmetics: the order was almost alphabetical, but not quite, so fix that
Uses QSettings directly. No defaults yet.
Sets these QCoreApplication elements in CLI client's main. Drops them
from QSettings ctor.
Sets app, org, and domain on remaining binaries (systray still missing).
ricab added 16 commits June 3, 2019 19:00
Drop petenv name constant and get it from the settings everywhere it is
used.
This implements the check directly in the Settings class. Such checks
should eventually be moved out to registered settings observers or
similar.
Makes the last remaining test pass.
To get meaningful messages if the issue arises accidentally outside of
the context of `get` or `set` commands.
Refrain from setting these centrally, to avoid changing results from
QStandardPaths (and so maintain previous record paths).
Avoids paths with 'Unknown Organization'.
VC++ would not accept WithArg (see https://is.gd/rT7foD), so replaced
plain gmock actions with lambda.
@ricab ricab force-pushed the settings-primary-name branch from 7ce0df8 to a16be82 Compare June 3, 2019 18:33
Copy link
Contributor

@gerboland gerboland 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 happy to take this as-is. It's a nice piece of code. Thank you!

Copy link
Contributor

@townsend2010 townsend2010 left a comment

Choose a reason for hiding this comment

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

This is some solid, good looking code and I learned some things from this:)

bors r=gerboland,townsend2010

bors bot added a commit that referenced this pull request Jun 4, 2019
805: Settings primary name r=gerboland,townsend2010 a=ricab

This addresses #766 

Co-authored-by: Ricardo Abreu <ricab@ricabhome.org>
@bors
Copy link
Contributor

bors bot commented Jun 4, 2019

@bors bors bot merged commit a16be82 into master Jun 4, 2019
@bors bors bot deleted the settings-primary-name branch June 4, 2019 16:27
bors bot added a commit that referenced this pull request Jun 7, 2019
824: Handle errors in reading/writting from/to persistent setttings r=townsend2010 a=ricab

This improves upon the code that was merged in #805 for #766 . I missed error handling and noticed when working on #776 .

Co-authored-by: Ricardo Abreu <ricab@ricabhome.org>
bors bot added a commit that referenced this pull request Jun 9, 2019
824: Handle errors in reading/writting from/to persistent setttings r=townsend2010 a=ricab

This improves upon the code that was merged in #805 for #766 . I missed error handling and noticed when working on #776 .

Co-authored-by: Ricardo Abreu <ricab@ricabhome.org>
bors bot added a commit that referenced this pull request Jun 10, 2019
824: Handle errors in reading/writting from/to persistent setttings r=townsend2010 a=ricab

This improves upon the code that was merged in #805 for #766 . I missed error handling and noticed when working on #776 .

Co-authored-by: Ricardo Abreu <ricab@ricabhome.org>
bors bot added a commit that referenced this pull request Jun 10, 2019
824: Handle errors in reading/writting from/to persistent setttings r=townsend2010 a=ricab

This improves upon the code that was merged in #805 for #766 . I missed error handling and noticed when working on #776 .

Co-authored-by: Ricardo Abreu <ricab@ricabhome.org>
bors bot added a commit that referenced this pull request Jun 10, 2019
824: Handle errors in reading/writting from/to persistent setttings r=townsend2010 a=ricab

This improves upon the code that was merged in #805 for #766 . I missed error handling and noticed when working on #776 .

Co-authored-by: Ricardo Abreu <ricab@ricabhome.org>
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.

Allow configuring the primary name

3 participants