-
Notifications
You must be signed in to change notification settings - Fork 406
Fix #1443: UnitParser.TryParse doesn't try with FallbackCulture (v6) #1466
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
Fix #1443: UnitParser.TryParse doesn't try with FallbackCulture (v6) #1466
Conversation
…ture - UnitParser: extracting a FindMatchingUnits function (used by both Parse and TryParse for a given abbreviation and culture) - UnitAbbreviationsCache: just a cosmetic change (flipping the order of the items in the tuple) - UnitParserTests: added the missing tests for the FallbackCulture
| [InlineData("г", "ru-RU", MassUnit.Gram)] | ||
| [InlineData("kg", "en-US", MassUnit.Kilogram)] | ||
| [InlineData("кг", "ru-RU", MassUnit.Kilogram)] | ||
| [InlineData("kg", "ru-RU", MassUnit.Kilogram)] // should work with the "FallbackCulture" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was passing before (but was never tested).
| [InlineData("г", "ru-RU", MassUnit.Gram)] | ||
| [InlineData("kg", "en-US", MassUnit.Kilogram)] | ||
| [InlineData("кг", "ru-RU", MassUnit.Kilogram)] | ||
| [InlineData("kg", "ru-RU", MassUnit.Kilogram)] // should work with the "FallbackCulture" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was failing before (the issue mentioned in #1443)
| /// <returns></returns> | ||
| public TUnitType Parse<TUnitType>(string unitAbbreviation, IFormatProvider? formatProvider = null) where TUnitType : Enum | ||
| public TUnitType Parse<TUnitType>(string unitAbbreviation, IFormatProvider? formatProvider = null) | ||
| where TUnitType : Enum |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is likely going to annoy some people (a little) but I really think we should make all Enum constraints have the struct, Enum - this makes sure that we're only calling this with a concrete type, disabling the potential call:
Enum whatType = UnitParser.Parse<Enum>(abbreviation, null);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see why not, I think it was a mistake to not restrict to struct on all methods. I see only some of them have it.
I'll push that as a separate PR.
|
In the "Test Session" try searching for |
| [InlineData("ru-RU", "Американская унция", VolumeUnit.UsOunce)] | ||
| [InlineData("fr-CA", "pmp", VolumeUnit.BoardFoot)] | ||
| [InlineData("fr-CA", "pied-planche", VolumeUnit.BoardFoot)] | ||
| [InlineData("fr-CA", "pied de planche", VolumeUnit.BoardFoot)] | ||
| public void ParseUnitWithCulture(string culture, string abbreviation, VolumeUnit expectedUnit) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to make it really explicit- we could aslo include the non-localized mappings for (culture, englishAbbreviation, unit):
[InlineData("fr-CA", "ac-ft", VolumeUnit.AcreFoot)]
[InlineData("fr-CA", "acre-foot", VolumeUnit.AcreFoot)]
[InlineData("fr-CA", "acre-feet", VolumeUnit.AcreFoot)]
....Change tests to control current culture: - Change `ParseUnit` to `ParseUnit_WithUsEnglishCurrentCulture` - Change `TryParseUnit` to `TryParseUnit_WithUsEnglishCurrentCulture` Add tests for more cases: - ParseUnit_WithUnsupportedCurrentCulture_FallsBackToUsEnglish - ParseUnit_WithCurrentCulture - TryParseUnit_WithUnsupportedCurrentCulture_FallsBackToUsEnglish - TryParseUnit_WithCurrentCulture
angularsen
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Superb! I pushed a few minor improvements to the PR.
| [InlineData("mg", AccelerationUnit.MillistandardGravity)] | ||
| [InlineData("nm/s²", AccelerationUnit.NanometerPerSecondSquared)] | ||
| [InlineData("g", AccelerationUnit.StandardGravity)] | ||
| public void TryParseUnit(string abbreviation, AccelerationUnit expectedUnit) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was not obvious to me why this won't fail if the current culture is ru-RU, but I see from the test generator that it picks units uniquely defined by only the fallback culture.
Maybe add a comment or rename the test method to help point this out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can also manipulate the current culture per test, and revert it back, to make the intent more clear and avoid different behavior for different host systems running the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pushed some improvements 70f1b3e
Change tests to control current culture:
- Change `ParseUnit` to `ParseUnit_WithUsEnglishCurrentCulture`
- Change `TryParseUnit` to `TryParseUnit_WithUsEnglishCurrentCulture`
Add tests for more cases:
- ParseUnit_WithUnsupportedCurrentCulture_FallsBackToUsEnglish
- ParseUnit_WithCurrentCulture
- TryParseUnit_WithUnsupportedCurrentCulture_FallsBackToUsEnglish
- TryParseUnit_WithCurrentCulture
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can also manipulate the current culture per test, and revert it back, to make the intent more clear and avoid different behavior for different host systems running the tests.
What, like the ToString method? Are you sure this is thread safe? I didn't want to risk another #438. I believe NUnit has annotations that can be used to set / ensure a given culture per test, but I don't know how this is supposed to be done properly in xUnit. Especially on a per-test level, I don't know if try / finally is the correct way to do it..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
XUnit runs each test per test class sequentially, so a try-finally or using-scope pattern to revert changes work well.
However, XUnit will concurrently run test classes by default, unless a test fixture is used to control concurrency. This is not a problem because each test class is run on a different thread, and any changes to CurrentCulture or fields marked with [ThreadStatic] attribute will be isolated per test class.
The only thing I'm not 100% about is whether XUnit creates new threads per test class or uses som kind of pool that may be tainted, but in my experience this has never been a problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, that's great- I've been tiptoeing around this issue for a long time..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So in what situation was the original issue occurring? There is no way that we could have been configuring something static on the per-test-class level..
Not sure what you mean, what issue exactly?
Something about CurrentCulture in tests that failed before?
Generally speaking, since tests can run on any developer or CI agent machine, it's good to control things like CurrentCulture to avoid flaky tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nah, when I saw the [Collection(nameof(UnitAbbreviationsCacheFixture))] (and read on the related issues) I thought that this is something that is required every time we need to change something static..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha, I didn't remember we had those.
// Apply this collection fixture to classes:
// 1. That rely on manipulating CultureInfo. See https://github.com/angularsen/UnitsNet/issues/436
// 2. To avoid accessing static ToString/Parse from multiple tests where UnitAbbreviationsCache.Default is modified
Nr 1 should not be a problem if you revert the value, not sure what happened back in 2018 but likely the order of test execution caused flakiness if we did not revert values.
Nr 2 is still required, since the static Default singleton and other static stuff is not marked [ThreadStatic]. If I recall correctly, a test project runs in the same app domain and share static fields, and most of our tests and test classes are in the same project.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nr 2 is still required, since the static
Defaultsingleton and other static stuff is not marked[ThreadStatic]. If I recall correctly, a test project runs in the same app domain and share static fields, and most of our tests and test classes are in the same project.
Yeah, that's still a bit of a pain in the ass - wish there was a simple annotation that would make a given class run in total isolation, like in BenchmarkDotNet.
…or) (#1595) - Refactored the `try / catch` blocks for the `Parse` tests, similarly to the way this was handled in #1466 - added two tests for the `GetAbbreviation` method: `GetAbbreviationForCulture` and `GetAbbreviationWithDefaultCulture` --------- Co-authored-by: Andreas Gullberg Larsen <[email protected]>
Fixes #1443
UnitParser: extracting aFindMatchingUnitsfunction (used by bothParseandTryParsefor a given abbreviation and culture)UnitAbbreviationsCache: just a cosmetic change (flipping the order of the items in the tuple)UnitParserTests: added the missing tests for theFallbackCultureUnitTestBaseClassGenerator: improving the code-coverage of theParseUnitandTryParseUnittests (parsing without specifying a culture, which should always work with abbreviations for theFallbackCulture)