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

null safety #50

Merged
merged 27 commits into from
Apr 7, 2021
Merged

null safety #50

merged 27 commits into from
Apr 7, 2021

Conversation

SteveAlexander
Copy link
Collaborator

@SteveAlexander SteveAlexander commented Dec 27, 2020

Hello!

Latest news on this branch, 28th March 2021:

As of this commit, there are 0 analysis / linter errors (pedantic rules), and 0 failing tests from dart test. So, I consider this branch worth checking out in more details.

I'm sure there are areas of the code I changed that could use some tidying up, changing in style, or outright fixing.

Also, I've often used the dart autoformatter, so there may be areas of code that the maintainers prefer to keep in a hand-crafted style rather than the autoformat style.

Similarly, I renamed some variables to conform to the "pedantic" analysis rules, but there might be reasons to revert these, and add a comment for the analyzer to ignore the rules.

√ time_machine % dart test
00:04 +0: loading test/datetimezone_local_conversions_test.dart
Operator Functions with Mirrors!
00:04 +0: loading test/calendars/era_test.dart
skipped ResourcePresence because unimplemented
Total Tests Skipped = 1;
00:09 +255: loading test/calendars/islamic_calendar_system_test.dart
skipped SampleDateBclCompatibility
skipped Constructor_InvalidEnumsForCoverage because Doesn't make sense in Dart
skipped BclThroughHistory
skipped GetInstance_Caching
skipped GetInstance_ArgumentValidation
Total Tests Skipped = 5;
00:14 +578: loading test/calendars/umAlQura_yearmonthday_calculator_test.dart
skipped BclEquivalence because Need Dart Equivalent Test
Total Tests Skipped = 1;
00:17 +615: loading test/calendars/persian_calendar_system_test.dart
skipped BclThroughHistory because Unsure how to implement
Total Tests Skipped = 1;
00:19 +644: loading test/localinstant_test.dart
Operator Functions with Mirrors!
00:20 +670: loading test/offset_date_test.dart
Operator Functions with Mirrors!
00:23 +690: loading test/interval_test.dart
Operator Functions with Mirrors!
00:23 +715: loading test/offset_datetime_test.dart
Operator Functions with Mirrors!
00:24 +718: loading test/offset_datetime_test.dart
skipped LocalComparer
skipped InstantComparer
Total Tests Skipped = 2;
00:26 +774: loading test/datetimezone_providers_test.dart
Operator Functions with Mirrors!
00:26 +774: loading test/instant_test.dart
Operator Functions with Mirrors!
00:28 +854: loading test/timezones/miscellaneous_bug_reports_test.dart
Operator Functions with Mirrors!
00:29 +859: loading test/timezones/fixed_datetimezone_test.dart
Operator Functions with Mirrors!
00:29 +864: test/timezones/zone_recurrence_test.dart: ZoneRecurrenceToString
name +01 ZoneYearOffset[mode:Utc monthOfYear:10 dayOfMonth:31 dayOfWeek:3 advance:true timeOfDay:00:00:00 addDay:false] [1900-2000]
00:30 +878: loading test/timezones/fixed_datetimezone_test.dart
skipped Read_NoNameInStream because unimplemented
skipped Roundtrip because unimplemented
skipped Read_WithNameInStream because unimplemented
Total Tests Skipped = 3;
00:31 +888: loading test/timezones/cached_datetimezone_test.dart
Operator Functions with Mirrors!
00:33 +894: loading test/timezones/tzdb_datetimezone_test.dart
Operator Functions with Mirrors!
00:33 +894: loading test/timezones/etc_test.dart
Operator Functions with Mirrors!
skipped FixedEasternZone because Aliases not yet available?
skipped FixedWesternZone because Aliases not yet available?
Total Tests Skipped = 2;
00:35 +1320: loading test/timezones/paris_test.dart
Operator Functions with Mirrors!
00:36 +1321: loading test/timezones/datetimezone_cache_test.dart
Operator Functions with Mirrors!
00:37 +1387: loading test/timezones/transition_test.dart
Operator Functions with Mirrors!
00:38 +1387: test/timezones/transition_test.dart: TransitionToString
Transition to +01 at 2017-08-25T15:26:30Z
00:40 +1389: loading test/timezones/jordan_test.dart
Operator Functions with Mirrors!
00:40 +1389: loading test/timezones/algiers_test.dart
Operator Functions with Mirrors!
00:44 +1443: loading test/timezones/zone_equality_comparer_test.dart
Operator Functions with Mirrors!
00:45 +1461: loading test/offset_time_test.dart
Operator Functions with Mirrors!
00:47 +1505: loading test/datetimezone_get_zoneintervals_test.dart
Operator Functions with Mirrors!
00:47 +1505: loading test/nano_duration_test.dart
Operator Functions with Mirrors!
00:48 +1559: loading test/offset_operators_test.dart
Operator Functions with Mirrors!
00:48 +1559: loading test/localdatetime_test.dart
Operator Functions with Mirrors!
00:50 +1610: loading test/annual_date_test.dart
Operator Functions with Mirrors!
00:53 +1623: loading test/zoneddatetime_test.dart
Operator Functions with Mirrors!
00:56 +1682: loading test/yearmonthday_and_calendar_test.dart
Operator Functions with Mirrors!
00:56 +1689: loading test/duration_test.dart
Operator Functions with Mirrors!
00:59 +1801: test/timemachine_init_test.dart: timezoneCanBeOverridden
Operator Functions with Mirrors!
01:02 +1820: loading test/yearmonthday_test.dart
Operator Functions with Mirrors!
01:03 +1848: test/localdate_pseudomutators_test.dart: PlusDays_OutOfRange.[-9998, 1, 1, -1]
RangeError: Date computation would underflow the minimum year of the calendar
01:03 +1849: test/localdate_pseudomutators_test.dart: PlusDays_OutOfRange.[-9996, 1, 1, -1000]
RangeError (daysSinceEpoch): Invalid value: Not in inclusive range -4371222..2932896: -4371492
01:03 +1850: test/localdate_pseudomutators_test.dart: PlusDays_OutOfRange.[9999, 12, 31, 1]
RangeError: Date computation would overflow the maximum year of the calendar
01:03 +1851: test/localdate_pseudomutators_test.dart: PlusDays_OutOfRange.[9997, 12, 31, 1000]
RangeError (daysSinceEpoch): Invalid value: Not in inclusive range -4371222..2932896: 2933166
01:03 +1852: test/localdate_pseudomutators_test.dart: PlusDays_OutOfRange.[2000, 1, 1, 2147483647]
RangeError (daysSinceEpoch): Invalid value: Not in inclusive range -4371222..2932896: 2147494604
01:03 +1853: test/localdate_pseudomutators_test.dart: PlusDays_OutOfRange.[1, 1, 1, -2147483648]
RangeError (daysSinceEpoch): Invalid value: Not in inclusive range -4371222..2932896: -2148202810
01:05 +1957: loading test/text/period_pattern_normalizing_iso_test.dart
Constructor called.
01:06 +2080: loading test/text/localdatetime_pattern_test.dart
Operator Functions with Mirrors!
Time to load cultures: 14 ms;
01:11 +6232: loading test/text/localtime_pattern_test.dart
Operator Functions with Mirrors!
01:11 +6634: loading test/text/localtime_pattern_test.dart
Time to load cultures: 21 ms;
01:15 +8924: loading test/text/zoneddatetime_pattern_test.dart
Operator Functions with Mirrors!
01:18 +9246: loading test/text/composite_pattern_builder_pattern_test.dart
skipped Enumerators because unimplemented
Total Tests Skipped = 1;
01:25 +9985: test/localdatetime_pseudomutators_test.dart: PlusHours_Overflow.[-9998, 1, 1, -1]
RangeError: Date computation would underflow the minimum year of the calendar
01:25 +9986: test/localdatetime_pseudomutators_test.dart: PlusHours_Overflow.[9999, 12, 31, 24]
RangeError: Date computation would overflow the maximum year of the calendar
01:25 +9987: test/localdatetime_pseudomutators_test.dart: PlusHours_Overflow.[1970, 1, 1, 9223372036854775807]
RangeError (daysSinceEpoch): Invalid value: Not in inclusive range -4371222..2932896: 384307168202282325
01:25 +9988: test/localdatetime_pseudomutators_test.dart: PlusHours_Overflow.[1970, 1, 1, -9223372036854775808]
RangeError (daysSinceEpoch): Invalid value: Not in inclusive range -4371222..2932896: -384307168202282326
01:25 +9999: loading test/globalization/culture_test.dart
Operator Functions with Mirrors!
01:26 +10001: All tests passed!
√ time_machine %

Previous comment on this branch from December 2020:

I've done a first cut at null safety on this branch. I have no idea if it all works! This branch is not intended for merging, or even necessarily working on. It's here to show what kind of work is involved in making this package null safe.

I've basically gone through removing all of the "Problems" highlighted by the analyzer. There remain 48 lint warnings. I think it will be clear what to do about these once someone with actual understanding of the code has looked over it.

There were lots of judgement calls about whether to have something return a nullable type or not. Also, I commented out most of the "@internal" markers as they no longer seem to do what's intended with the latest Dart.

@rich-j
Copy link
Contributor

rich-j commented Feb 8, 2021

@SteveAlexander, thanks for this conversion. We're able to use this with our null safety conversion with a few changes. To use change your pubspec.yaml dependency to reference your above pull request:

  time_machine:    #^0.9.16
    git:
      url: https://github.com/Dana-Ferguson/time_machine
      ref: b336f38

We also had to change a few declarations from late to nullable. In file timezones/tzdb_datetimezone_source.dart line 26 needs to be nullable instead of late:

  static DateTimeZoneProvider? _defaultProvider;

In file text/globalization/time_machine_format_info.dart lines 33-43 also need to be nullable:

  FixedFormatInfoPatternParser<Time>? _timePatternParser;
<...snip 9 lines...>
  FixedFormatInfoPatternParser<AnnualDate>? _annualDatePatternParser;

We can now run :-). We do get 6 warnings at compile time like:

/lib/src/skipped_time_error.dart:39:72: Warning: Operand of null-aware operation '?.' has type 'DateTimeZone' which excludes null.
 - 'DateTimeZone' is from 'package:time_machine/src/datetimezone.dart'.
      : message = "Local time $localDateTime is invalid in time zone ${zone?.id ?? 'null'}";

@Dana-Ferguson any thoughts on the timeline for a null safe version?

Thanks all!!

@TatsuUkraine
Copy link

@Dana-Ferguson any plans to move this forward?

@SteveAlexander
Copy link
Collaborator Author

I updated the PR description with the latest status — all tests from dart test pass, at least on my machine.

@SteveAlexander SteveAlexander changed the title first cut at null safety null safety Mar 28, 2021
@rich-j
Copy link
Contributor

rich-j commented Apr 3, 2021

@SteveAlexander thanks for the migration to null safety. We are now able to run with "full null safety" - hooray. However, we're having trouble with testing, particularly mocking with Mockito.

Both issues are with Mockito's overrides not being "valid" and occur on several TimeMachine objects (e.g. Instant, Time, LocalTime). The issue for Instant is:

error: 'Object.==' ('bool Function(Object)') isn't a valid concrete implementation of 'Instant.==' ('bool Function(dynamic)'). (invalid_implementation_override at [xxx] test/objects/obj_test.mocks.dart:88)

This is because the TimeMachine override of operator== declares the parameter as dynamic but the underlying object parameter definition is a non-nullable object. Here is the Mockito bug about the same issue with other libraries:
dart-lang/mockito#354. Changing the == parameter type from dynamic to object across all TimeMachine objects would fix this one. There should be no negative ramifications for this change (the above issue has a link to the same change being made to FlutterFire).

The second issue is similar but for toString:

error: 'Object.toString' ('String Function()') isn't a valid concrete implementation of 'Instant.toString' ('String Function([String?, Culture?])'). (invalid_implementation_override at [xxx] test/objects/obj_test.mocks.dart:88)

This second one is also mismatched parameter definitions but the fix doesn't seem as straightforward... It would seem the cleanest fix would be to introduce another function to be the official string formatting function and change the overridden object toString function to have no parameters <-- obviously an API breaking change.

Mockito has an old related issue dart-lang/mockito#228 that suggests overriding the definition but that is for the previous version that doesn't generate code. We see this problem on mocking objects (e.g. Person) that just return a property of type Instant (e.g. createdTimestamp). We'll keep exploring but would like to hear if anyone else has a workaround.

@SteveAlexander
Copy link
Collaborator Author

Hi @rich-j

Thanks for describing these issues. The first one, about the equality operator, makes sense to me. I’m expecting it would be a straightforward change in time machine, and one we should make.

The second issue, about toString, I’m not so sure about. The Dart language specification (section 11.2.2, Correct Member Overrides, and section 20.4.4 rule 15) says that String Function([any optional parameters]) is a valid subtype of String Function() . So it seems to me that time machine is doing something reasonable here, something that Mockito might accommodate. Would you agree, or perhaps I’ve misunderstood something?

@rich-j
Copy link
Contributor

rich-j commented Apr 4, 2021

@SteveAlexander thanks for all of the work. I agree that the toString parameter issue is really a Mockito issue. Thanks for the pointer to the language spec.

Update - gave up on Mockito (too many issues generating mocks) and went with Mocktail.

@rich-j
Copy link
Contributor

rich-j commented Apr 6, 2021

@SteveAlexander - Getting closer to everything running null safe. We have a set of tests that run locally but fail when we run them in GitHub actions. It seems that GitHub actions running on ubuntu doesn't have a valid locale defined so calling TimeMachine.initialize(); fails with:

Failed to load "test/a_test.dart": Null check operator used on a null value
  package:time_machine/src/platforms/vm.dart 99:39  TimeMachine.initialize

Lines 96-100

   // Default Culture
   var cultureId = io.Platform.localeName.split('.').first.replaceAll('_', '-');
   Culture? culture = await Cultures.getCulture(cultureId);
   ICultures.currentCulture = culture!;       // <-- the non-null assertion here fails
   // todo: remove Culture.currentCulture

explicitly assumes the culture is found. It also has a todo to possibly remove this.

We need some way to get past initialization. One thought is to use the initialize method argument map to pass a culture value. The map does accept a timeZone override, a culture override would be useful for testing.

@SteveAlexander
Copy link
Collaborator Author

Do you know what io.Platform.localeName is on that OS?

@rich-j
Copy link
Contributor

rich-j commented Apr 6, 2021

  var locale = io.Platform.localeName;
  print("locale '$locale'");

printed locale 'C.UTF-8'

https://unix.stackexchange.com/questions/597962/how-widespread-is-the-c-utf-8-locale

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.

4 participants