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

Cop idea: Prefer #change over #round for Time/DateTime #1715

Open
ydakuka opened this issue Sep 11, 2023 · 9 comments
Open

Cop idea: Prefer #change over #round for Time/DateTime #1715

ydakuka opened this issue Sep 11, 2023 · 9 comments

Comments

@ydakuka
Copy link

ydakuka commented Sep 11, 2023

Describe the solution you'd like

# bad
expect(user.created_at).to eq Time.now.utc.round

# good
expect(user.created_at).to eq Time.now.utc.change(nsec: 0)

Rubocop

ydakuka@yauhenid:~/Work/project$ bin/rails_docker rubocop -V
1.56.2 (using Parser 3.2.2.3, rubocop-ast 1.29.0, running on ruby 2.7.8) [x86_64-linux]
  - rubocop-capybara 2.18.0
  - rubocop-factory_bot 2.23.1
  - rubocop-performance 1.19.0
  - rubocop-rails 2.21.0
  - rubocop-rake 0.6.0
  - rubocop-rspec 2.24.0
  - rubocop-thread_safety 0.5.1
@ydakuka ydakuka changed the title Cop idea: Prefer #change over #round Cop idea: Prefer #change over #round Sep 11, 2023
@ydakuka ydakuka changed the title Cop idea: Prefer #change over #round Cop idea: Prefer #change over #round for Time/DateTime Sep 11, 2023
@pirj
Copy link
Member

pirj commented Sep 11, 2023

‘change’ is a Rails thing? And it’s probably a good recommendation for Rails code in general, not just specs?

Would ‘round’ be always identical to ‘change’ (i mean floor/ceil)?

Good to move this to rubocop-rails?

@ydakuka
Copy link
Author

ydakuka commented Sep 11, 2023

‘change’ is a Rails thing?

Yes, it should be a RSpec/Rails cop.

And it’s probably a good recommendation for Rails code in general, not just specs?

Perhaps, but personally I had such problems only in specs.

Would ‘round’ be always identical to ‘change’ (i mean floor/ceil)?

No,

irb(main):002:0> Time.parse('2023-09-11 16:01:15.497457202 UTC').round.to_i
=> 1694448075
irb(main):001:0> Time.parse('2023-09-11 16:01:15.517457202 UTC').round.to_i
=> 1694448076

irb(main):003:0> Time.parse('2023-09-11 16:01:15.497457202 UTC').floor.to_i
=> 1694448075
irb(main):004:0> Time.parse('2023-09-11 16:01:15.517457202 UTC').floor.to_i
=> 1694448075

irb(main):005:0> Time.parse('2023-09-11 16:01:15.497457202 UTC').ceil.to_i
=> 1694448076
irb(main):006:0> Time.parse('2023-09-11 16:01:15.517457202 UTC').ceil.to_i
=> 1694448076

I think if rounding affects the result, we need to use Timecop or look for problems in the code.

Good to move this to rubocop-rails?

Yep.

@pirj
Copy link
Member

pirj commented Sep 11, 2023

I see, thanks for this research.

With what you say, it seems that indeed, this cop makes more sense in Rails RSpec specs.

I recall that the the default timestamp nanosecond precision varies across DBs, and it was discarding nanoseconds in MySQL until recently. But how this is actually done? Does it round down at milliseconds?

How does it even work if user.created_at has millisecond precision, and the result of ‘round’ has whole seconds precision?

With this cop our ultimate goal is to avoid flakiness, right?

@ydakuka
Copy link
Author

ydakuka commented Sep 14, 2023

Does it round down at milliseconds?

No,

For PostgreSQL:

When Rails models are saved to the database, any timestamps they have are stored using a type in PostgreSQL called timestamp without time zone, which has microsecond resolution—i.e., six digits after the decimal. So when 1577987974.6472975 is sent to PostgreSQL, it truncates the last digit of the fractional part and instead saves 1577987974.647297.

Reference: https://www.toptal.com/ruby-on-rails/timestamp-truncation-rails-activerecord-tale#the-cause

For MySQL:

Rails will remove fractional part if mysql adapter does not support precision.

If precision is not set then fractional part gets stripped and date is not changed.

Reference: https://www.bigbinary.com/blog/rails-5-handles-datetime-with-better-precision

If mysql adapter supports precision, rails will drop nanoseconds:

create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
  t.datetime "updated_at", precision: 6, null: false
end
irb(main):001:0> ActiveRecord::Base.connection.select_value('SELECT version()')
=> "10.4.30-MariaDB-1:10.4.30+maria~ubu2004-log"
irb(main):002:0> ActiveRecord::Base.connection.execute("UPDATE `users` SET `updated_at` = '2016-01-18 23:59:59.999999999' WHERE `users`.`id` = 1")
=> nil
irb(main):003:0> User.find(1).updated_at.utc
=> 2016-01-18 23:59:59.999999 UTC

@ydakuka
Copy link
Author

ydakuka commented Sep 14, 2023

Also rails drops microseconds in job argument assertions

rails/rails#35713

@ydakuka
Copy link
Author

ydakuka commented Sep 14, 2023

How does it even work if user.created_at has millisecond precision, and the result of ‘round’ has whole seconds precision?

I don't get your question.

@pirj
Copy link
Member

pirj commented Sep 26, 2023

user.created_at - floating point, 6 to nine digits of precision
tTime.now.utc.round - integer, zero precision

Chances of ‘eq’ to match are one in a million.

Why do we even need this cop then?

@ydakuka
Copy link
Author

ydakuka commented Oct 3, 2023

The main idea of this cop is to prevent skipping precision for datetime fields. I believe that the test above should be written more reliably.

# bad, another example
expect(user.created_at.to_i).to eq Time.now.utc.to_i

P.S. I think the old problem is no longer relevant.

@pirj
Copy link
Member

pirj commented Oct 3, 2023

Could ‘change’ be considered as an option to write time comparisons more reliably?

I suggest starting from scratch, getting examples of unreliable specs, and finding a common solution we could suggest everyone to use.

This seems related and a good starting point rails/rails#38831

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

No branches or pull requests

2 participants