Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Slightly Less Painful Time Zones (RailsConf)

Slightly Less Painful Time Zones (RailsConf)

Abstract:
For developers, there are two things that are certain for time zones: you can’t avoid having to deal with them, and you will screw them up at some point. There are, however, some ways to mitigate the pain. This talk will discuss tactics for avoiding time zone mayhem, using a feature to send out weekly email reports in a customer’s local time zone as a case study. It will cover idiosyncrasies of how time zones are handled in Ruby and Rails, how to write tests to avoid false positives, and advice on how to release time zone-related code changes more safely.

This talk was presented at RailsConf 2015 in Atlanta, ~30 minutes: https://www.youtube.com/watch?v=VFDurYw6aZ8
Blog post: http://kwugirl.blogspot.com/2015/04/slightly-less-painful-time-zones.html

Katherine Wu

April 21, 2015
Tweet

More Decks by Katherine Wu

Other Decks in Programming

Transcript

  1. Slightly Less Painful Time Zones
    Katherine Wu (KWu)
    @kwugirl.
    !
    !
    Software Engineer

    View full-size slide

  2. !
    @kwugirl
    why time is hard
    the problem
    the proposed solution
    the actual solution

    View full-size slide

  3. !
    @kwugirl
    why time is hard
    http://xkcd.com/1514/

    View full-size slide

  4. !
    @kwugirl
    https://twitter.com/endtwist/status/544598751449739265

    View full-size slide

  5. !
    @kwugirl
    wat

    View full-size slide

  6. !
    @kwugirl
    “in October 1582, since days 5..14 just do not exist.”
    http://guides.rubyonrails.org/v4.0.13/active_support_core_extensions.html#extensions-to-date

    View full-size slide

  7. !
    @kwugirl
    wat

    View full-size slide

  8. !
    @kwugirl
    falsehoods programmers believe about time
    http://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time
    http://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time

    View full-size slide

  9. !
    @kwugirl
    falsehoods programmers believe about time
    http://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time
    http://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time

    View full-size slide

  10. !
    @kwugirl
    falsehoods programmers believe about time
    • There are only 24 time zones.
    http://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time
    http://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time

    View full-size slide

  11. !
    @kwugirl
    falsehoods programmers believe about time
    • There are only 24 time zones.
    • Time zones always differ by a
    whole hour.
    http://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time
    http://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time

    View full-size slide

  12. !
    @kwugirl
    falsehoods programmers believe about time
    • There are only 24 time zones.
    • Time zones always differ by a
    whole hour.
    • Time always goes forwards.
    http://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time
    http://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time

    View full-size slide

  13. !
    @kwugirl
    wat

    View full-size slide

  14. !
    @kwugirl
    Lesson #1: Trust NOTHING

    View full-size slide

  15. !
    @kwugirl
    Lesson #1: Trust NOTHING
    (including Argentina)

    View full-size slide

  16. !
    @kwugirl
    [1] pry(main)> 1.month == 30.days

    View full-size slide

  17. !
    @kwugirl
    [1] pry(main)> 1.month == 30.days
    => true

    View full-size slide

  18. !
    @kwugirl
    [1] pry(main)> 1.month == 30.days
    => true
    [2] pry(main)> Date.today

    View full-size slide

  19. !
    @kwugirl
    [1] pry(main)> 1.month == 30.days
    => true
    [2] pry(main)> Date.today
    => Thu, 09 Apr 2015

    View full-size slide

  20. !
    @kwugirl
    [1] pry(main)> 1.month == 30.days
    => true
    [2] pry(main)> Date.today
    => Thu, 09 Apr 2015
    [3] pry(main)> Date.today - 1.month

    View full-size slide

  21. !
    @kwugirl
    [1] pry(main)> 1.month == 30.days
    => true
    [2] pry(main)> Date.today
    => Thu, 09 Apr 2015
    [3] pry(main)> Date.today - 1.month
    => Mon, 09 Mar 2015

    View full-size slide

  22. !
    @kwugirl
    [1] pry(main)> 1.month == 30.days
    => true
    [2] pry(main)> Date.today
    => Thu, 09 Apr 2015
    [3] pry(main)> Date.today - 1.month
    => Mon, 09 Mar 2015
    [4] pry(main)> Date.today - 30.days

    View full-size slide

  23. !
    @kwugirl
    [1] pry(main)> 1.month == 30.days
    => true
    [2] pry(main)> Date.today
    => Thu, 09 Apr 2015
    [3] pry(main)> Date.today - 1.month
    => Mon, 09 Mar 2015
    [4] pry(main)> Date.today - 30.days
    => Tue, 10 Mar 2015

    View full-size slide

  24. !
    @kwugirl
    Lesson #1: Trust NOTHING
    (including Argentina)

    View full-size slide

  25. !
    @kwugirl
    Lesson #1: Trust NOTHING
    (including Argentina)
    (including math)

    View full-size slide

  26. !
    @kwugirl
    the problem

    View full-size slide

  27. !
    @kwugirl
    weekly email report

    View full-size slide

  28. !
    @kwugirl
    cron job

    View full-size slide

  29. !
    @kwugirl
    cron job
    WeeklyEmailJob for account 1
    WeeklyEmailJob for account 2
    WeeklyEmailJob for account 3…
    enqueues

    View full-size slide

  30. !
    @kwugirl
    cron job
    WeeklyEmailJob for account 1
    WeeklyEmailJob for account 2
    WeeklyEmailJob for account 3… email for account 1
    email for account 2
    email for account 3…
    enqueues
    generates

    View full-size slide

  31. !
    @kwugirl
    cron job
    WeeklyEmailJob for account 1
    WeeklyEmailJob for account 2
    WeeklyEmailJob for account 3… email for account 1
    email for account 2
    email for account 3…
    enqueues
    SCHEDULING
    generates

    View full-size slide

  32. !
    @kwugirl
    cron job
    WeeklyEmailJob for account 1
    WeeklyEmailJob for account 2
    WeeklyEmailJob for account 3… email for account 1
    email for account 2
    email for account 3…
    enqueues
    SCHEDULING EXECUTING
    generates

    View full-size slide

  33. !
    @kwugirl
    cron job
    enqueues
    email for account 1
    email for account 2
    email for account 3…
    generates
    WeeklyEmailJob for account 1
    WeeklyEmailJob for account 2
    WeeklyEmailJob for account 3…

    View full-size slide

  34. !
    @kwugirl
    cron job
    enqueues
    Monday 10am Pacific time
    email for account 1
    email for account 2
    email for account 3…
    generates
    WeeklyEmailJob for account 1
    WeeklyEmailJob for account 2
    WeeklyEmailJob for account 3…

    View full-size slide

  35. !
    @kwugirl
    however…

    View full-size slide

  36. !
    @kwugirl
    Monday 10am Pacific time == Tuesday 2am Tokyo
    Monday Tuesday

    View full-size slide

  37. !
    @kwugirl
    MOAR ACCOUNTS

    View full-size slide

  38. !
    @kwugirl
    MOAR ACCOUNTS

    View full-size slide

  39. !
    @kwugirl
    “Monday” report 

    received on Tuesday!
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  40. !
    @kwugirl
    the proposed solution

    View full-size slide

  41. !
    @kwugirl
    set up report jobs to run Monday 1:01am local time
    Local time Pacific time
    WeeklyEmailJob for
    account 1
    Mon 1:01am JST Sun 9:01am
    WeeklyEmailJob for
    account 2
    Mon 1:01am EDT Sun 10:01pm
    WeeklyEmailJob for
    account 3
    Mon 1:01am HAST Mon 4:01am

    View full-size slide

  42. !
    @kwugirl
    cron job
    WeeklyEmailJob for account 1
    WeeklyEmailJob for account 2
    WeeklyEmailJob for account 3…
    enqueues
    email for account 1
    email for account 2
    email for account 3…
    generates
    local

    View full-size slide

  43. !
    @kwugirl
    cron job
    WeeklyEmailJob for account 1
    WeeklyEmailJob for account 2
    WeeklyEmailJob for account 3…
    enqueues
    Saturday 10am Pacific time
    email for account 1
    email for account 2
    email for account 3…
    generates
    local

    View full-size slide

  44. !
    @kwugirl
    report processing spread out
    0
    25
    50
    75
    100
    8am PT 9am PT 10am PT 11am PT 12pm PT 1pm PT 2pm PT 3pm PT
    before

    View full-size slide

  45. !
    @kwugirl
    report processing spread out
    0
    25
    50
    75
    100
    8am PT 9am PT 10am PT 11am PT 12pm PT 1pm PT 2pm PT 3pm PT
    before
    0
    25
    50
    75
    100
    8am PT 9am PT 10am PT 11am PT 12pm PT 1pm PT 2pm PT 3pm PT
    after

    View full-size slide

  46. !
    @kwugirl
    first attempt

    View full-size slide

  47. !
    @kwugirl
    what date is the next Monday?

    View full-size slide

  48. !
    @kwugirl
    [1] pry(main)> Date.today

    View full-size slide

  49. !
    @kwugirl
    [1] pry(main)> Date.today
    => Sun, 05 Apr 2015

    View full-size slide

  50. !
    @kwugirl
    [1] pry(main)> Date.today
    => Sun, 05 Apr 2015
    [2] pry(main)> DateTime.parse("Monday, 1:01")

    View full-size slide

  51. !
    @kwugirl
    [1] pry(main)> Date.today
    => Sun, 05 Apr 2015
    [2] pry(main)> DateTime.parse("Monday, 1:01")
    => Mon, 06 Apr 2015 01:01:00 +0000

    View full-size slide

  52. !
    @kwugirl
    how do I get that into 

    the local time zone?

    View full-size slide

  53. !
    @kwugirl
    Rails Time/Date classes

    View full-size slide

  54. !
    @kwugirl
    TZInfo
    daylight saving-aware time conversions

    View full-size slide

  55. !
    @kwugirl
    Internet Assigned Numbers Authority
    (IANA)
    Time Zone Database

    View full-size slide

  56. !
    @kwugirl
    April 2015
    May 2012

    View full-size slide

  57. !
    @kwugirl
    ActiveRecord pins TZInfo version

    View full-size slide

  58. !
    @kwugirl
    ActiveRecord pins TZInfo version
    (need to upgrade Rails versions to get accurate data)

    View full-size slide

  59. !
    @kwugirl
    ActiveSupport::TimeZone

    View full-size slide

  60. !
    @kwugirl
    ActiveSupport::TimeZone
    TZInfo::Timezone instances

    View full-size slide

  61. !
    @kwugirl
    “a meaningful subset of 

    146 time zones” 

    (out of many more)

    View full-size slide

  62. !
    @kwugirl
    ~24 bands vs…

    View full-size slide

  63. !
    @kwugirl
    { "International Date Line West" => "Pacific/Midway", "Midway Island" => "Pacific/Midway", "American Samoa" => "Pacific/Pago_Pago", "Hawaii" => "Pacific/Honolulu", "Alaska" => "America/Juneau", "Pacific Time (US & Canada)" =>
    "America/Los_Angeles", "Tijuana" => "America/Tijuana", "Mountain Time (US & Canada)" => "America/Denver", "Arizona" => "America/Phoenix", "Chihuahua" => "America/Chihuahua", "Mazatlan" => "America/Mazatlan", "Central Time (US &
    Canada)" => "America/Chicago", "Saskatchewan" => "America/Regina", "Guadalajara" => "America/Mexico_City", "Mexico City" => "America/Mexico_City", "Monterrey" => "America/Monterrey", "Central America" => "America/Guatemala", "Eastern
    Time (US & Canada)" => "America/New_York", "Indiana (East)" => "America/Indiana/Indianapolis", "Bogota" => "America/Bogota", "Lima" => "America/Lima", "Quito" => "America/Lima", "Atlantic Time (Canada)" => "America/Halifax",
    "Caracas" => "America/Caracas", "La Paz" => "America/La_Paz", "Santiago" => "America/Santiago", "Newfoundland" => "America/St_Johns", "Brasilia" => "America/Sao_Paulo", "Buenos Aires" => "America/Argentina/Buenos_Aires", "Montevideo"
    => "America/Montevideo", "Georgetown" => "America/Guyana", "Greenland" => "America/Godthab", "Mid-Atlantic" => "Atlantic/South_Georgia", "Azores" => "Atlantic/Azores", "Cape Verde Is." => "Atlantic/Cape_Verde", "Dublin" => "Europe/
    Dublin", "Edinburgh" => "Europe/London", "Lisbon" => "Europe/Lisbon", "London" => "Europe/London", "Casablanca" => "Africa/Casablanca", "Monrovia" => "Africa/Monrovia", "UTC" => "Etc/UTC", "Belgrade" => "Europe/Belgrade",
    "Bratislava" => "Europe/Bratislava", "Budapest" => "Europe/Budapest", "Ljubljana" => "Europe/Ljubljana", "Prague" => "Europe/Prague", "Sarajevo" => "Europe/Sarajevo", "Skopje" => "Europe/Skopje", "Warsaw" => "Europe/Warsaw", "Zagreb"
    => "Europe/Zagreb", "Brussels" => "Europe/Brussels", "Copenhagen" => "Europe/Copenhagen", "Madrid" => "Europe/Madrid", "Paris" => "Europe/Paris", "Amsterdam" => "Europe/Amsterdam", "Berlin" => "Europe/Berlin", "Bern" => "Europe/
    Berlin", "Rome" => "Europe/Rome", "Stockholm" => "Europe/Stockholm", "Vienna" => "Europe/Vienna", "West Central Africa" => "Africa/Algiers", "Bucharest" => "Europe/Bucharest", "Cairo" => "Africa/Cairo", "Helsinki" => "Europe/
    Helsinki", "Kyiv" => "Europe/Kiev", "Riga" => "Europe/Riga", "Sofia" => "Europe/Sofia", "Tallinn" => "Europe/Tallinn", "Vilnius" => "Europe/Vilnius", "Athens" => "Europe/Athens", "Istanbul" => "Europe/Istanbul", "Minsk" => "Europe/
    Minsk", "Jerusalem" => "Asia/Jerusalem", "Harare" => "Africa/Harare", "Pretoria" => "Africa/Johannesburg", "Kaliningrad" => "Europe/Kaliningrad", "Moscow" => "Europe/Moscow", "St. Petersburg" => "Europe/Moscow", "Volgograd" =>
    "Europe/Volgograd", "Samara" => "Europe/Samara", "Kuwait" => "Asia/Kuwait", "Riyadh" => "Asia/Riyadh", "Nairobi" => "Africa/Nairobi", "Baghdad" => "Asia/Baghdad", "Tehran" => "Asia/Tehran", "Abu Dhabi" => "Asia/Muscat", "Muscat" =>
    "Asia/Muscat", "Baku" => "Asia/Baku", "Tbilisi" => "Asia/Tbilisi", "Yerevan" => "Asia/Yerevan", "Kabul" => "Asia/Kabul", "Ekaterinburg" => "Asia/Yekaterinburg", "Islamabad" => "Asia/Karachi", "Karachi" => "Asia/Karachi", "Tashkent"
    => "Asia/Tashkent", "Chennai" => "Asia/Kolkata", "Kolkata" => "Asia/Kolkata", "Mumbai" => "Asia/Kolkata", "New Delhi" => "Asia/Kolkata", "Kathmandu" => "Asia/Kathmandu", "Astana" => "Asia/Dhaka", "Dhaka" => "Asia/Dhaka", "Sri
    Jayawardenepura" => "Asia/Colombo", "Almaty" => "Asia/Almaty", "Novosibirsk" => "Asia/Novosibirsk", "Rangoon" => "Asia/Rangoon", "Bangkok" => "Asia/Bangkok", "Hanoi" => "Asia/Bangkok", "Jakarta" => "Asia/Jakarta", "Krasnoyarsk" =>
    "Asia/Krasnoyarsk", "Beijing" => "Asia/Shanghai", "Chongqing" => "Asia/Chongqing", "Hong Kong" => "Asia/Hong_Kong", "Urumqi" => "Asia/Urumqi", "Kuala Lumpur" => "Asia/Kuala_Lumpur", "Singapore" => "Asia/Singapore", "Taipei" => "Asia/
    Taipei", "Perth" => "Australia/Perth", "Irkutsk" => "Asia/Irkutsk", "Ulaanbaatar" => "Asia/Ulaanbaatar", "Seoul" => "Asia/Seoul", "Osaka" => "Asia/Tokyo", "Sapporo" => "Asia/Tokyo", "Tokyo" => "Asia/Tokyo", "Yakutsk" => "Asia/
    Yakutsk", "Darwin" => "Australia/Darwin", "Adelaide" => "Australia/Adelaide", "Canberra" => "Australia/Melbourne", "Melbourne" => "Australia/Melbourne", "Sydney" => "Australia/Sydney", "Brisbane" => "Australia/Brisbane", "Hobart" =>
    "Australia/Hobart", "Vladivostok" => "Asia/Vladivostok", "Guam" => "Pacific/Guam", "Port Moresby" => "Pacific/Port_Moresby", "Magadan" => "Asia/Magadan", "Srednekolymsk" => "Asia/Srednekolymsk", "Solomon Is." => "Pacific/
    Guadalcanal", "New Caledonia" => "Pacific/Noumea", "Fiji" => "Pacific/Fiji", "Kamchatka" => "Asia/Kamchatka", "Marshall Is." => "Pacific/Majuro", "Auckland" => "Pacific/Auckland", "Wellington" => "Pacific/Auckland", "Nuku'alofa" =>
    "Pacific/Tongatapu", "Tokelau Is." => "Pacific/Fakaofo", "Chatham Is." => "Pacific/Chatham", "Samoa" => "Pacific/Apia" }
    ~24 bands vs…

    View full-size slide

  64. !
    @kwugirl
    friendlier zone names
    “Eastern Time (US & Canada)” vs “America/New_York”

    View full-size slide

  65. !
    @kwugirl
    getting time zone names

    View full-size slide

  66. !
    @kwugirl
    getting time zone names
    [1] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z|
    z.name if z.formatted_offset == "+12:00"}.compact!

    View full-size slide

  67. !
    @kwugirl
    getting time zone names
    [1] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z|
    z.name if z.formatted_offset == "+12:00"}.compact!
    => ["Fiji", "Kamchatka", "Marshall Is.", "Auckland", “Wellington"]

    View full-size slide

  68. !
    @kwugirl
    getting time zone names
    [1] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z|
    z.name if z.formatted_offset == "+12:00"}.compact!
    => ["Fiji", "Kamchatka", "Marshall Is.", "Auckland", “Wellington"]
    [2] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z|
    z.name if z.formatted_offset == "-11:00"}.compact!

    View full-size slide

  69. !
    @kwugirl
    getting time zone names
    [1] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z|
    z.name if z.formatted_offset == "+12:00"}.compact!
    => ["Fiji", "Kamchatka", "Marshall Is.", "Auckland", “Wellington"]
    [2] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z|
    z.name if z.formatted_offset == "-11:00"}.compact!
    => ["International Date Line West", "Midway Island", "American
    Samoa"]

    View full-size slide

  70. !
    @kwugirl
    (no UTC -12:00 offset)

    View full-size slide

  71. !
    @kwugirl
    (no UTC -12:00 offset)
    “The Baker and Howland islands are not there. 

    But as (wikipedia says) they are uninhabited, they
    would be a quite specific use case. 

    I don't think they're going to be added to TZInfo.”
    — dmathieu

    https://github.com/rails/rails/issues/11390

    View full-size slide

  72. !
    @kwugirl
    how do I get a DateTime 

    into the local time zone?

    View full-size slide

  73. !
    @kwugirl
    ActiveSupport::TimeZone
    !
    local_to_utc(time, dst=true)
    Adjust the given time to the simultaneous time in UTC.
    Returns a Time.utc() instance.
    !

    View full-size slide

  74. !
    @kwugirl
    attempt #1’s code

    View full-size slide

  75. !
    @kwugirl
    attempt #1’s code
    def local_1am(account_time_zone)

    View full-size slide

  76. !
    @kwugirl
    attempt #1’s code
    def local_1am(account_time_zone)
    dt = DateTime.parse("Monday, 1:01”)

    View full-size slide

  77. !
    @kwugirl
    attempt #1’s code
    def local_1am(account_time_zone)
    dt = DateTime.parse("Monday, 1:01”)
    default = ActiveSupport::TimeZone['Pacific Time (US & Canada)']

    View full-size slide

  78. !
    @kwugirl
    attempt #1’s code
    def local_1am(account_time_zone)
    dt = DateTime.parse("Monday, 1:01”)
    default = ActiveSupport::TimeZone['Pacific Time (US & Canada)']
    local_time_zone = account_time_zone || default


    View full-size slide

  79. !
    @kwugirl
    attempt #1’s code
    def local_1am(account_time_zone)
    dt = DateTime.parse("Monday, 1:01”)
    default = ActiveSupport::TimeZone['Pacific Time (US & Canada)']
    local_time_zone = account_time_zone || default

    local_time_zone.local_to_utc(dt)
    end

    View full-size slide

  80. !
    @kwugirl
    attempt #1’s tests

    View full-size slide

  81. !
    @kwugirl
    context "enqueue time" do

    View full-size slide

  82. !
    @kwugirl
    context "enqueue time" do
    setup do

    saturday_pt = DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")

    Timecop.freeze(saturday_pt)

    end


    View full-size slide

  83. !
    @kwugirl
    context "enqueue time" do
    setup do

    saturday_pt = DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")

    Timecop.freeze(saturday_pt)

    end

    # your tests


    View full-size slide

  84. !
    @kwugirl
    context "enqueue time" do
    setup do

    saturday_pt = DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")

    Timecop.freeze(saturday_pt)

    end

    # your tests

    teardown do

    Timecop.return

    end

    end

    View full-size slide

  85. !
    @kwugirl
    # Running:
    !
    ..
    !
    Finished in -37452054.639617s, -0.0000 runs/s, -0.0000
    assertions/s.
    !
    2 runs, 4 assertions, 0 failures, 0 errors, 0 skips

    View full-size slide

  86. !
    @kwugirl
    # Running:
    !
    ..
    !
    Finished in -37452054.639617s, -0.0000 runs/s, -0.0000
    assertions/s.
    !
    2 runs, 4 assertions, 0 failures, 0 errors, 0 skips

    View full-size slide

  87. !
    @kwugirl
    wat

    View full-size slide

  88. !
    @kwugirl
    setup do

    saturday_pt = DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")

    Timecop.freeze(saturday_pt)

    end


    View full-size slide

  89. !
    @kwugirl
    setup do

    saturday_pt = DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")

    Timecop.freeze(saturday_pt)

    end

    should "be the next Monday 1:01am local time (west of UTC)" do

    View full-size slide

  90. !
    @kwugirl
    setup do

    saturday_pt = DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")

    Timecop.freeze(saturday_pt)

    end

    should "be the next Monday 1:01am local time (west of UTC)" do
    mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"]

    View full-size slide

  91. !
    @kwugirl
    setup do

    saturday_pt = DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")

    Timecop.freeze(saturday_pt)

    end

    should "be the next Monday 1:01am local time (west of UTC)" do
    mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"]
    expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00")

    View full-size slide

  92. !
    @kwugirl
    setup do

    saturday_pt = DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")

    Timecop.freeze(saturday_pt)

    end

    should "be the next Monday 1:01am local time (west of UTC)" do
    mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"]
    expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00")
    assert_equal expected, local_1am(mtn_tz)
    end

    View full-size slide

  93. !
    @kwugirl
    UTC
    Mountain time Bangkok time

    View full-size slide

  94. !
    @kwugirl
    # Running:
    !
    ..
    !
    Finished in 0.059444s, 33.6451 runs/s, 33.6451
    assertions/s
    !
    2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

    View full-size slide

  95. !
    @kwugirl
    scheduling changes

    View full-size slide

  96. !
    @kwugirl
    before
    cron job
    enqueues
    Monday 10am Pacific time
    email for account 1
    email for account 2
    email for account 3…
    generates
    WeeklyEmailJob for account 1
    WeeklyEmailJob for account 2
    WeeklyEmailJob for account 3…

    View full-size slide

  97. !
    @kwugirl
    after
    cron job
    WeeklyEmailJob for account 1
    WeeklyEmailJob for account 2
    WeeklyEmailJob for account 3…
    enqueues
    Saturday 10am Pacific time
    email for account 1
    email for account 2
    email for account 3…
    generates
    local

    View full-size slide

  98. !
    @kwugirl
    start on Sat morning
    if pdt_day == SATURDAY
    base_email = 'script/run_weekly_email_report.rb'
    Async::Command::Bulk.new(base_email).enqueue
    end

    View full-size slide

  99. !
    @kwugirl
    queue up jobs with a time
    WeeklyEmailReportJob.new(account.id).enqueue_at(local_1am(tz))

    View full-size slide

  100. !
    @kwugirl
    FAILURE

    View full-size slide

  101. !
    @kwugirl
    had to manually regenerate 

    all email reports

    View full-size slide

  102. !
    @kwugirl
    Lesson #2: Have a backup plan

    View full-size slide

  103. !
    @kwugirl
    unknown unknowns

    View full-size slide

  104. !
    @kwugirl
    Lesson #3: Do an internal test run

    View full-size slide

  105. !
    @kwugirl
    so what happened?

    View full-size slide

  106. !
    @kwugirl
    that week’s emails were queued for
    1:01am local time…last Monday

    View full-size slide

  107. !
    @kwugirl
    can’t tell whether it’s closest date or last date
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  108. !
    @kwugirl
    [1] pry(main)> Date.today
    can’t tell whether it’s closest date or last date
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  109. !
    @kwugirl
    [1] pry(main)> Date.today
    => Thu, 09 Apr 2015
    can’t tell whether it’s closest date or last date
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  110. !
    @kwugirl
    [1] pry(main)> Date.today
    => Thu, 09 Apr 2015
    can’t tell whether it’s closest date or last date
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  111. !
    @kwugirl
    [1] pry(main)> Date.today
    => Thu, 09 Apr 2015
    [2] pry(main)> DateTime.parse("Monday 1:01")
    can’t tell whether it’s closest date or last date
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  112. !
    @kwugirl
    [1] pry(main)> Date.today
    => Thu, 09 Apr 2015
    [2] pry(main)> DateTime.parse("Monday 1:01")
    can’t tell whether it’s closest date or last date
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  113. !
    @kwugirl
    [1] pry(main)> Date.today
    => Thu, 09 Apr 2015
    [2] pry(main)> DateTime.parse("Monday 1:01")
    => Mon, 06 Apr 2015 01:01:00 +0000
    can’t tell whether it’s closest date or last date
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  114. !
    @kwugirl
    [1] pry(main)> Date.today
    => Thu, 09 Apr 2015
    [2] pry(main)> DateTime.parse("Monday 1:01")
    => Mon, 06 Apr 2015 01:01:00 +0000
    can’t tell whether it’s closest date or last date
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  115. !
    @kwugirl
    Lesson #4: Today’s console results
    are not tomorrow’s console results

    View full-size slide

  116. !
    @kwugirl
    but…tests??

    View full-size slide

  117. !
    @kwugirl
    DateTime.parse not friends
    with Timecop

    View full-size slide

  118. !
    @kwugirl
    changed test setup

    View full-size slide

  119. !
    @kwugirl
    changed test setup
    saturday_pt = DateTime.parse("1st May 2015 10:05:00 AM -07:00")

    View full-size slide

  120. !
    @kwugirl
    changed test setup
    saturday_pt = DateTime.parse("1st May 2015 10:05:00 AM -07:00")
    Timecop.freeze(saturday_pt)

    View full-size slide

  121. !
    @kwugirl
    changed test setup
    saturday_pt = DateTime.parse("1st May 2015 10:05:00 AM -07:00")
    Timecop.freeze(saturday_pt)
    setting date to May 1st (Fri)!
    want to get May 4th (next Mon)

    View full-size slide

  122. !
    @kwugirl
    [1] pry(#)> Time.now

    View full-size slide

  123. !
    @kwugirl
    [1] pry(#)> Time.now
    => 2015-05-01 10:05:00 -0700


    View full-size slide

  124. !
    @kwugirl
    [1] pry(#)> Time.now
    => 2015-05-01 10:05:00 -0700


    date set to May 1st

    View full-size slide

  125. !
    @kwugirl
    [1] pry(#)> Time.now
    => 2015-05-01 10:05:00 -0700


    [2] pry(#)> DateTime.parse("Monday, 1:01")
    date set to May 1st

    View full-size slide

  126. !
    @kwugirl
    [1] pry(#)> Time.now
    => 2015-05-01 10:05:00 -0700


    [2] pry(#)> DateTime.parse("Monday, 1:01")
    => Mon, 06 Apr 2015 01:01:00 +0000
    date set to May 1st

    View full-size slide

  127. !
    @kwugirl
    [1] pry(#)> Time.now
    => 2015-05-01 10:05:00 -0700


    [2] pry(#)> DateTime.parse("Monday, 1:01")
    => Mon, 06 Apr 2015 01:01:00 +0000
    date set to May 1st
    DateTime parses to Apr 6th?!?

    View full-size slide

  128. !
    @kwugirl
    setup do

    saturday_pt = DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")

    Timecop.freeze(saturday_pt)

    end

    should "be the next Monday 1:01am local time (west of UTC)" do
    mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"]
    expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00")
    !
    assert_equal expected, local_1am(mtn_tz)
    end

    View full-size slide

  129. !
    @kwugirl
    setup do

    saturday_pt = DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")

    Timecop.freeze(saturday_pt)

    end

    should "be the next Monday 1:01am local time (west of UTC)" do
    mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"]
    expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00")
    !
    assert_equal expected, local_1am(mtn_tz)
    end

    View full-size slide

  130. !
    @kwugirl
    setup do

    saturday_pt = DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")

    Timecop.freeze(saturday_pt)

    end

    should "be the next Monday 1:01am local time (west of UTC)" do
    mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"]
    expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00")
    !
    assert_equal expected, local_1am(mtn_tz)
    end
    Test only passed because happened to run in the
    same week of Apr 4th, not because of Timecop.freeze

    View full-size slide

  131. !
    @kwugirl
    Lesson #5: Guard against
    writing false positive tests

    View full-size slide

  132. !
    @kwugirl
    actual solution

    View full-size slide

  133. !
    @kwugirl
    TDD, that’s a thing

    View full-size slide

  134. !
    @kwugirl
    setup do

    saturday_pt = DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")

    Timecop.freeze(saturday_pt)

    end

    should "be the next Monday 1:01am local time (west of UTC)" do
    mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"]
    expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00")
    !
    assert_equal expected, local_1am(mtn_tz)
    end

    View full-size slide

  135. !
    @kwugirl
    setup do

    saturday_pt = DateTime.parse("8th Feb 2015 10:05:00 AM -07:00")

    Timecop.freeze(saturday_pt)

    end

    should "be the next Monday 1:01am local time (west of UTC)" do
    mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"]
    expected = DateTime.parse("10th Feb 2015 01:01:00 AM -06:00")
    !
    assert_equal expected, local_1am(mtn_tz)
    end

    View full-size slide

  136. !
    @kwugirl
    setup do

    saturday_pt = DateTime.parse("8th Feb 2015 10:05:00 AM -07:00")

    Timecop.freeze(saturday_pt)

    end

    should "be the next Monday 1:01am local time (west of UTC)" do
    mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"]
    expected = DateTime.parse("10th Feb 2015 01:01:00 AM -06:00")
    assert_wday(expected, "Monday")
    !
    assert_equal expected, local_1am(mtn_tz)
    end

    View full-size slide

  137. !
    @kwugirl
    # Running:
    !
    FF
    !
    Finished in 0.061686s, 32.4223 runs/s, 64.8445 assertions/s.
    !
    1) Failure: SchedulingHelperTest#test_: enqueue time should be the
    next Monday 1:01am local time (west of UTC).
    -Mon, 10 Feb 2015 01:01:00 -0600
    +Mon, 06 Apr 2015 07:01:00 +0000
    !
    2) Failure: SchedulingHelperTest#test_: enqueue time should be the
    next Monday 1:01am local time (east of UTC).
    -Mon, 10 Feb 2015 01:01:00 +0700
    +Sun, 05 Apr 2015 18:01:00 +0000
    !
    !
    2 runs, 4 assertions, 2 failures, 0 errors, 0 skips

    View full-size slide

  138. !
    @kwugirl
    need another way to get “next Monday” 

    besides simple DateTime.parse

    View full-size slide

  139. !
    @kwugirl
    Lesson #2: Have a backup plan

    View full-size slide

  140. !
    @kwugirl
    might need to run script after
    Saturday, if Saturday job fails

    View full-size slide

  141. !
    @kwugirl
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  142. !
    @kwugirl
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  143. !
    @kwugirl
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  144. !
    @kwugirl
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  145. !
    @kwugirl
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  146. !
    @kwugirl
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  147. !
    @kwugirl
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  148. !
    @kwugirl
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  149. !
    @kwugirl
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  150. !
    @kwugirl
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  151. !
    @kwugirl
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  152. !
    @kwugirl
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  153. !
    @kwugirl
    distinguishing between “this”
    week and “next” week?

    View full-size slide

  154. !
    @kwugirl
    multiple time zones

    View full-size slide

  155. !
    @kwugirl
    multiple time zones
    • the time we’re used to thinking of (Pacific time)

    View full-size slide

  156. !
    @kwugirl
    multiple time zones
    • the time we’re used to thinking of (Pacific time)
    • server’s time zone (who even knows)

    View full-size slide

  157. !
    @kwugirl
    multiple time zones
    • the time we’re used to thinking of (Pacific time)
    • server’s time zone (who even knows)
    • the customer’s time zone (could be ahead of us
    or behind us when running script)

    View full-size slide

  158. !
    @kwugirl
    need logic to apply globally

    View full-size slide

  159. !
    @kwugirl
    if it’s still Monday somewhere, anywhere,
    we’ll think of it as “this” week still

    View full-size slide

  160. !
    @kwugirl
    once it’s at least Tuesday everywhere,
    script should schedule reports for the
    next Monday

    View full-size slide

  161. !
    @kwugirl
    Lesson #6: Figure out a
    consistent, global logic

    View full-size slide

  162. !
    @kwugirl
    def local_1am(account_time_zone)

    View full-size slide

  163. !
    @kwugirl
    def local_1am(account_time_zone)
    last_day = ActiveSupport::TimeZone['International Date Line West'].today

    View full-size slide

  164. !
    @kwugirl
    def local_1am(account_time_zone)
    last_day = ActiveSupport::TimeZone['International Date Line West'].today
    days_until_monday = (8 - last_day.wday) % 7

    View full-size slide

  165. !
    @kwugirl
    def local_1am(account_time_zone)
    last_day = ActiveSupport::TimeZone['International Date Line West'].today
    days_until_monday = (8 - last_day.wday) % 7
    next_monday = last_day + days_until_monday


    View full-size slide

  166. !
    @kwugirl
    def local_1am(account_time_zone)
    last_day = ActiveSupport::TimeZone['International Date Line West'].today
    days_until_monday = (8 - last_day.wday) % 7
    next_monday = last_day + days_until_monday

    local_tz = account_time_zone || ActiveSupport::TimeZone['Pacific Time']

    View full-size slide

  167. !
    @kwugirl
    def local_1am(account_time_zone)
    last_day = ActiveSupport::TimeZone['International Date Line West'].today
    days_until_monday = (8 - last_day.wday) % 7
    next_monday = last_day + days_until_monday

    local_tz = account_time_zone || ActiveSupport::TimeZone['Pacific Time']
    current_tz_offset = local_timezone.formatted_offset

    View full-size slide

  168. !
    @kwugirl
    def local_1am(account_time_zone)
    last_day = ActiveSupport::TimeZone['International Date Line West'].today
    days_until_monday = (8 - last_day.wday) % 7
    next_monday = last_day + days_until_monday

    local_tz = account_time_zone || ActiveSupport::TimeZone['Pacific Time']
    current_tz_offset = local_timezone.formatted_offset
    like “+07:00”

    View full-size slide

  169. !
    @kwugirl
    def local_1am(account_time_zone)
    last_day = ActiveSupport::TimeZone['International Date Line West'].today
    days_until_monday = (8 - last_day.wday) % 7
    next_monday = last_day + days_until_monday

    local_tz = account_time_zone || ActiveSupport::TimeZone['Pacific Time']
    current_tz_offset = local_timezone.formatted_offset

    DateTime.parse("#{next_monday} 01:01:00 AM #{current_tz_offset}")
    end

    View full-size slide

  170. !
    @kwugirl
    # Running:
    !
    ..
    !
    Finished in 0.041562s, 48.1209 runs/s, 96.2418
    assertions/s.
    !
    2 runs, 4 assertions, 0 failures, 0 errors, 0 skips

    View full-size slide

  171. !
    @kwugirl
    MOAR TESTS

    View full-size slide

  172. !
    @kwugirl
    context "enqueue time" do
    context "during U.S. standard time" do…
    end
    !
    context "during U.S. daylight saving" do…
    end

    View full-size slide

  173. !
    @kwugirl
    # Running:
    !
    ..F.
    !
    Finished in 0.061256s, 65.2997 runs/s, 130.5995 assertions/s.
    !
    1) Failure:
    SchedulingHelperTest#test_: enqueue time during U.S. daylight saving
    should be the next Monday 1:01am local time (west of UTC).
    --- expected
    +++ actual
    @@ -1 +1 @@
    -Mon, 09 Jun 2014 01:01:00 -0600
    +Mon, 09 Jun 2014 01:01:00 -0700
    !
    !
    4 runs, 8 assertions, 1 failures, 0 errors, 0 skips

    View full-size slide

  174. !
    @kwugirl
    # Running:
    !
    ..F.
    !
    Finished in 0.061256s, 65.2997 runs/s, 130.5995 assertions/s.
    !
    1) Failure:
    SchedulingHelperTest#test_: enqueue time during U.S. daylight saving
    should be the next Monday 1:01am local time (west of UTC).
    --- expected
    +++ actual
    @@ -1 +1 @@
    -Mon, 09 Jun 2014 01:01:00 -0600
    +Mon, 09 Jun 2014 01:01:00 -0700
    !
    !
    4 runs, 8 assertions, 1 failures, 0 errors, 0 skips
    off by 1…

    View full-size slide

  175. !
    @kwugirl
    def local_1am(account_time_zone)
    last_day = ActiveSupport::TimeZone['International Date Line West'].today
    days_until_monday = (8 - last_day.wday) % 7
    next_monday = last_day + days_until_monday

    local_tz = account_time_zone || ActiveSupport::TimeZone['Pacific Time']
    current_tz_offset = local_timezone.now.formatted_offset

    DateTime.parse("#{next_monday} 01:01:00 AM #{current_tz_offset}")
    end

    View full-size slide

  176. !
    @kwugirl
    # Running:
    !
    ....
    !
    Finished in 0.043776s, 91.3743 runs/s, 182.7485
    assertions/s.
    !
    4 runs, 8 assertions, 0 failures, 0 errors, 0 skips

    View full-size slide

  177. !
    @kwugirl
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  178. !
    @kwugirl
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  179. !
    @kwugirl
    context "when run on different days of the week" do
    setup do
    @samoa_tz = ActiveSupport::TimeZone[“American Samoa"]
    @wellington_tz = ActiveSupport::TimeZone["Wellington"]
    end

    View full-size slide

  180. !
    @kwugirl
    should "be the next day when run on late Sunday night" do

    View full-size slide

  181. !
    @kwugirl
    should "be the next day when run on late Sunday night" do
    sunday_night = DateTime.parse("1st Jun 2014 11:30:00 PM
    #{@samoa_tz.formatted_offset}")

    Timecop.freeze(sunday_night)

    assert_wday(@samoa_tz.now, "Sunday")

    View full-size slide

  182. !
    @kwugirl
    should "be the next day when run on late Sunday night" do
    sunday_night = DateTime.parse("1st Jun 2014 11:30:00 PM
    #{@samoa_tz.formatted_offset}")

    Timecop.freeze(sunday_night)

    assert_wday(@samoa_tz.now, "Sunday")

    View full-size slide

  183. !
    @kwugirl
    should "be the next day when run on late Sunday night" do
    sunday_night = DateTime.parse("1st Jun 2014 11:30:00 PM
    #{@samoa_tz.formatted_offset}")

    Timecop.freeze(sunday_night)

    assert_wday(@samoa_tz.now, "Sunday")
    # It's already Monday in the customer account's time zone

    assert_wday(@wellington_tz.now, "Monday")


    View full-size slide

  184. !
    @kwugirl
    should "be the next day when run on late Sunday night" do
    sunday_night = DateTime.parse("1st Jun 2014 11:30:00 PM
    #{@samoa_tz.formatted_offset}")

    Timecop.freeze(sunday_night)

    assert_wday(@samoa_tz.now, "Sunday")
    # It's already Monday in the customer account's time zone

    assert_wday(@wellington_tz.now, "Monday")

    local_tz = ActiveSupport::TimeZone[@wellington_tz.name]

    expected = DateTime.parse("2nd Jun 2014 01:01:00 AM +12:00")


    View full-size slide

  185. !
    @kwugirl
    should "be the next day when run on late Sunday night" do
    sunday_night = DateTime.parse("1st Jun 2014 11:30:00 PM
    #{@samoa_tz.formatted_offset}")

    Timecop.freeze(sunday_night)

    assert_wday(@samoa_tz.now, "Sunday")
    # It's already Monday in the customer account's time zone

    assert_wday(@wellington_tz.now, "Monday")

    local_tz = ActiveSupport::TimeZone[@wellington_tz.name]

    expected = DateTime.parse("2nd Jun 2014 01:01:00 AM +12:00")

    assert_equal expected, local_1am(local_tz)

    end

    View full-size slide

  186. !
    @kwugirl
    # Running:
    !
    .....
    !
    Finished in 0.059667s, 83.7984 runs/s, 201.1162
    assertions/s.
    !
    5 runs, 12 assertions, 0 failures, 0 errors, 0 skips

    View full-size slide

  187. !
    @kwugirl
    should "be the same week when still Monday somewhere" do

    View full-size slide

  188. !
    @kwugirl
    should "be the same week when still Monday somewhere" do
    monday = DateTime.parse("2nd Jun 2014 11:30:00 PM
    #{@samoa_tz.formatted_offset}")

    Timecop.freeze(monday)

    assert_wday(@samoa_tz.now, "Monday")

    View full-size slide

  189. !
    @kwugirl
    should "be the same week when still Monday somewhere" do
    monday = DateTime.parse("2nd Jun 2014 11:30:00 PM
    #{@samoa_tz.formatted_offset}")

    Timecop.freeze(monday)

    assert_wday(@samoa_tz.now, "Monday")

    local_tz = ActiveSupport::TimeZone[@wellington_tz.name]


    View full-size slide

  190. !
    @kwugirl
    should "be the same week when still Monday somewhere" do
    monday = DateTime.parse("2nd Jun 2014 11:30:00 PM
    #{@samoa_tz.formatted_offset}")

    Timecop.freeze(monday)

    assert_wday(@samoa_tz.now, "Monday")

    local_tz = ActiveSupport::TimeZone[@wellington_tz.name]

    expected = DateTime.parse("2nd Jun 2014 01:01:00 AM +12:00")

    assert_wday(expected, "Monday")

    View full-size slide

  191. !
    @kwugirl
    should "be the same week when still Monday somewhere" do
    monday = DateTime.parse("2nd Jun 2014 11:30:00 PM
    #{@samoa_tz.formatted_offset}")

    Timecop.freeze(monday)

    assert_wday(@samoa_tz.now, "Monday")

    local_tz = ActiveSupport::TimeZone[@wellington_tz.name]

    expected = DateTime.parse("2nd Jun 2014 01:01:00 AM +12:00")

    assert_wday(expected, "Monday")

    assert_equal expected, local_1am(local_tz)
    end

    View full-size slide

  192. !
    @kwugirl
    # Running:
    !
    ......
    !
    Finished in 0.059173s, 101.3976 runs/s, 253.4940
    assertions/s.
    !
    6 runs, 15 assertions, 0 failures, 0 errors, 0 skips

    View full-size slide

  193. !
    @kwugirl
    Sun Mon Tues Wed Thurs Fri Sat

    View full-size slide

  194. !
    @kwugirl
    should "be the next week when run on Tuesday" do

    View full-size slide

  195. !
    @kwugirl
    should "be the next week when run on Tuesday" do
    tuesday = DateTime.parse("3rd Jun 2014 00:30:00 AM
    #{@samoa_tz.formatted_offset}")

    Timecop.freeze(tuesday)

    assert_wday(tuesday, "Tuesday")

    View full-size slide

  196. !
    @kwugirl
    should "be the next week when run on Tuesday" do
    tuesday = DateTime.parse("3rd Jun 2014 00:30:00 AM
    #{@samoa_tz.formatted_offset}")

    Timecop.freeze(tuesday)

    assert_wday(tuesday, "Tuesday")

    local_tz = ActiveSupport::TimeZone[@wellington_tz.name]


    View full-size slide

  197. !
    @kwugirl
    should "be the next week when run on Tuesday" do
    tuesday = DateTime.parse("3rd Jun 2014 00:30:00 AM
    #{@samoa_tz.formatted_offset}")

    Timecop.freeze(tuesday)

    assert_wday(tuesday, "Tuesday")

    local_tz = ActiveSupport::TimeZone[@wellington_tz.name]

    expected = DateTime.parse("9th Jun 2014 01:01:00 AM +12:00")

    assert_wday(expected, "Monday")


    View full-size slide

  198. !
    @kwugirl
    should "be the next week when run on Tuesday" do
    tuesday = DateTime.parse("3rd Jun 2014 00:30:00 AM
    #{@samoa_tz.formatted_offset}")

    Timecop.freeze(tuesday)

    assert_wday(tuesday, "Tuesday")

    local_tz = ActiveSupport::TimeZone[@wellington_tz.name]

    expected = DateTime.parse("9th Jun 2014 01:01:00 AM +12:00")

    assert_wday(expected, "Monday")

    assert_equal expected, local_1am(local_tz)
    end

    View full-size slide

  199. !
    @kwugirl
    # Running:
    !
    .......
    !
    Finished in 0.060848s, 115.0408 runs/s, 295.8191
    assertions/s.
    !
    7 runs, 18 assertions, 0 failures, 0 errors, 0 skips

    View full-size slide

  200. !
    @kwugirl
    tests — Before
    • context "enqueue time"
    • should "be the next Monday 1:01am local time (west
    of UTC)"
    • should "be the next Monday 1:01am local time (east
    of UTC)"

    View full-size slide

  201. !
    @kwugirl
    tests — After
    • context "enqueue time"
    • context "during U.S. standard time”
    • should "be the next Monday 1:01am local time (west of UTC)"
    • should "be the next Monday 1:01am local time (east of UTC)"
    • context "during U.S. daylight saving”
    • should "be the next Monday 1:01am local time (west of UTC)"
    • should "be the next Monday 1:01am local time (east of UTC)”
    • context "when run on different days of the week"
    • should "be the next day when run on late Sunday night"
    • should "be the same week when still Monday somewhere"
    • should "be the next week when run on Tuesday"

    View full-size slide

  202. !
    @kwugirl
    tests to write
    • covering time zones west/east of UTC
    • pin down exactly when you’re starting and when you’re ending
    • pick random dates in the past or in the future--not current dates
    • test dates inside/outside daylight saving period
    • check that the dates being used in your tests are the expected day
    of the week
    • test when triggering the scripts on different days of the week
    • test on dates of daylight saving transitions

    View full-size slide

  203. !
    @kwugirl
    scheduling process changes

    View full-size slide

  204. !
    @kwugirl
    Lesson #2: Have a backup plan

    View full-size slide

  205. !
    @kwugirl
    keep option to trigger manually
    if @options[:enqueue_later]
    WeeklyEmailJob.new(id).enqueue_at(local_1am(tz))
    else
    WeeklyEmailJob.new(id).enqueue
    end

    View full-size slide

  206. !
    @kwugirl
    Lesson #3: Do an internal test run

    View full-size slide

  207. !
    @kwugirl
    # Test for enqueueing weekly emails jobs on Saturday
    if pdt_day == SATURDAY
    base = "script/run_weekly_email_report.rb"

    View full-size slide

  208. !
    @kwugirl
    # Test for enqueueing weekly emails jobs on Saturday
    if pdt_day == SATURDAY
    base = "script/run_weekly_email_report.rb"

    options = " --account 3,10 --enqueue_later"

    View full-size slide

  209. !
    @kwugirl
    # Test for enqueueing weekly emails jobs on Saturday
    if pdt_day == SATURDAY
    base = "script/run_weekly_email_report.rb"

    options = " --account 3,10 --enqueue_later"
    recipient = " --to \"[email protected]\""

    View full-size slide

  210. !
    @kwugirl
    # Test for enqueueing weekly emails jobs on Saturday
    if pdt_day == SATURDAY
    base = "script/run_weekly_email_report.rb"

    options = " --account 3,10 --enqueue_later"
    recipient = " --to \"[email protected]\""

    test_email = [base, options, recipient].join("")
    Async::Command::Bulk.new(test_email).enqueue
    end

    View full-size slide

  211. !
    @kwugirl
    lessons
    1. Trust NOTHING!
    2. Have a backup plan!
    3. Do an internal test run!
    4. Today’s console results are not 

    tomorrow’s console results !
    5. Guard against writing false positive tests!
    6. Figure out a consistent, global logic

    View full-size slide