Search code examples
ruby-on-railsjenkinsserializationprotocol-bufferstimecop

Protobuf-related unit tests passing on my local machine but failing in Jenkins pipeline


I have a set of unit tests for serializer objects in my Rails application. These serializer objects use the google-protobuf (~> 3.5) gem, including the Google::Protobuf::Timestamp object. For time-related attributes (like purchase_order#created_at, line_item#created_at, and inspection_event#event_occured_at), we use a TimeSerializer object, implemented as follows:

# frozen_string_literal: true

module ProtoSerializers
  class TimeSerializer < BaseProtoSerializer
    def serialize
      return if object.nil?

      GOOGLE_BASE::Timestamp.new(seconds: object&.to_i, nanos: object&.nsec)
    end
  end
end

This is instantiated by calling ProtoSerializers::TimeSerializer.serialize(time), where time is a Rails Time or DateTime object.

The tests compare expected results of the serialization with the actual results, pass if the results match, and fail otherwise:

describe '#serialize an inspection whose purchase order and line item are both archived' do

  subject { described_class.serialize(object) }

  let(:purchase_order) { create(:purchase_order, :is_archived) }
  let(:line_item) { create(:line_item, :archived, purchase_order: purchase_order) }
  let(:object) { create(:inspection, line_item: line_item) }

  it 'serializes attributes' do
    expect(subject).to be_a(MyCorp::Proto::MyApp::InspectionEvent)

    expect(subject).to have_attributes(
      ...(misc key-value pairs)...
      purchase_order: ProtoSerializers::PurchaseOrderSerializer.serialize(purchase_order),
      line_item: ProtoSerializers::LineItemSerializer.serialize(line_item),
      event_occurred_at: ProtoSerializers::TimeSerializer.serialize(object.event_occurred_at)
    )
  end
end

The PurchaseOrder and LineItem models both have created_at attributes, as per standard Rails practice.

This test passes on my machine but fails when I push it up to Github (which kicks off a Jenkins test pipeline). The expected-vs-actual diff appears as follows:

20:00:39        -:line_item => <MyCorp::Proto::MyApp::LineItem: ..., created_at: <Google::Protobuf::Timestamp: seconds: 1522368034, nanos: 909710602>, ...>,
20:00:39        +:line_item => <MyCorp::Proto::MyApp::LineItem: ..., created_at: <Google::Protobuf::Timestamp: seconds: 1522368034, nanos: 909710000>, ...>,
20:00:39        -:purchase_order => <MyCorp::Proto::MyApp::PurchaseOrder: ..., created_at: <Google::Protobuf::Timestamp: seconds: 1522368034, nanos: 909710602>>,
20:00:39        +:purchase_order => <MyCorp::Proto::MyApp::PurchaseOrder: ..., created_at: <Google::Protobuf::Timestamp: seconds: 1522368034, nanos: 909710000>>,

As you can see, the seconds attribute matches, but the nanos attrivute is off by a few hundred nanoseconds. I've tried using Timecop in this test, as follows, but the failed tests persisted:

before { Timecop.freeze(Time.now) }

after { Timecop.return }

I'm not sure what could be different between the Jenkins pipeline and my machine. I'm using a Macbook with an Intel Core i7 processor, which I believe is 64-bit.


Solution

  • It appears the cause is a loss of precision when converting 64-bit ints on my machine (Macbook with Intel Core i7 processor) to Protobufs 32-bit int nanoseconds. In order to fix the problem, I had to mock the time to something where a loss of precision wouldn't be a factor. In the end I used Epoch time, as follows:

    before { Timecop.freeze(Time.at(0)) }
    
    after { Timecop.return }
    

    This solved the issue.