diff --git a/tests/.gitattributes b/tests/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..1bccc1fa88e2220dd67a7d9c94e7cc6711b853ef
--- /dev/null
+++ b/tests/.gitattributes
@@ -0,0 +1 @@
+*.h5 filter=lfs diff=lfs merge=lfs -text
diff --git a/tests/RNSCA_npp_d20170912_t0001170_e0001370_b30441_c20170913220340173580_nobu_ops.h5 b/tests/RNSCA_npp_d20170912_t0001170_e0001370_b30441_c20170913220340173580_nobu_ops.h5
new file mode 100644
index 0000000000000000000000000000000000000000..5056cbf5c05c53f811a7093b5ca217561016841e
--- /dev/null
+++ b/tests/RNSCA_npp_d20170912_t0001170_e0001370_b30441_c20170913220340173580_nobu_ops.h5
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b88bdfe866b9ff01e107abfc60b62213c30f2869ac37106d4cb4c04895964e77
+size 36664
diff --git a/tests/test_rdrgen.py b/tests/test_rdrgen.py
new file mode 100644
index 0000000000000000000000000000000000000000..7d79e2e8e54f09c9d6f45a9f85bed97783ddd1af
--- /dev/null
+++ b/tests/test_rdrgen.py
@@ -0,0 +1,63 @@
+from datetime import datetime
+import os
+import subprocess
+from StringIO import StringIO
+
+import h5py
+
+import edosl0util.rdrgen as m
+from edosl0util.jpssrdr import decode_rdr_blob
+from edosl0util.stream import jpss_packet_stream
+
+snpp_base_time = 1698019234000000  # from CDFCB vol I "Time of First Ascending Node" table
+cris_gran_len = 31997000
+
+
+def test_calc_iet_granule():
+    run = lambda t: m.calc_iet_granule(snpp_base_time, cris_gran_len, t)
+    gran = 1880240293174000
+    prev_gran = 1880240261177000
+    next_gran = 1880240325171000
+    assert run(gran) == gran
+    assert run(gran + 1) == gran
+    assert run(gran - 1) == prev_gran
+    assert run(gran + cris_gran_len) == next_gran
+
+
+def test_calc_iet_aggregate():
+    grans_per_aggr = 15
+    run = lambda t: m.calc_iet_aggregate(snpp_base_time, cris_gran_len, grans_per_aggr, t)
+    aggr = 1880240037198000
+    aggr_len = grans_per_aggr * cris_gran_len
+    assert run(aggr - 1) == aggr - aggr_len
+    assert run(aggr) == aggr
+    assert run(aggr + aggr_len - 1) == aggr
+    assert run(aggr + aggr_len) == aggr + aggr_len
+
+
+def test_can_reproduce_rdr_from_class():
+    class_rdr_file = 'RNSCA_npp_d20170912_t0001170_e0001370_b30441_c20170913220340173580_nobu_ops.h5'
+    class_rdr_path = os.path.join(os.path.dirname(__file__), class_rdr_file)
+
+    # read buffer of raw packets from the RDR
+    with h5py.File(class_rdr_path, 'r') as class_h5_file:
+        class_blob = (
+            class_h5_file['All_Data/SPACECRAFT-DIARY-RDR_All/RawApplicationPackets_0'][:])
+        rdr_info = decode_rdr_blob(class_blob)
+        ini = rdr_info.header.ap_storage_offset
+        fin = ini + rdr_info.header.next_pkt_pos
+        pkt_buf = class_blob[ini:fin]
+
+    # generate new RDR from packets, injecting matching metadata from CLASS file
+    blob = m.build_rdr_blob('snpp', jpss_packet_stream(StringIO(pkt_buf.tobytes())))
+    m.write_rdr('snpp', blob, distributor='arch', origin='nob-', domain='ops',
+                creation_time=datetime(2017, 9, 13, 22, 3, 40, 173580),
+                gran_creation_time=datetime(2017, 9, 12, 1, 37, 43, 474383),
+                orbit_number=30441, software_ver='I2.0.03.00')
+
+    # use h5diff to verify files match. -p option is needed to allow some slop
+    # in comparing N_Percent_Missing_Data
+    p = subprocess.Popen(['h5diff', '-c', '-p', '1e-6', class_rdr_path, 'rdr.h5'],
+                         stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    assert p.communicate()[0] == ''
+    assert p.returncode == 0