Refactor to common file reader
diff --git a/test/distance_constraint_test.cpp b/test/distance_constraint_test.cpp
index 8b031ef..529198a 100644
--- a/test/distance_constraint_test.cpp
+++ b/test/distance_constraint_test.cpp
@@ -12,7 +12,7 @@
     REQUIRE(fp != nullptr);
 
     fseek(fp, 0, SEEK_END);
-    auto length = ftell(fp);
+    const size_t length = ftell(fp);
     fseek(fp, 0, SEEK_SET);
     uint8_t* bytes = new uint8_t[length];
     REQUIRE(fread(bytes, 1, length, fp) == length);
@@ -50,4 +50,4 @@
 
     delete file;
     delete[] bytes;
-}
\ No newline at end of file
+}
diff --git a/test/file_test.cpp b/test/file_test.cpp
index 7744e2a..d81d67a 100644
--- a/test/file_test.cpp
+++ b/test/file_test.cpp
@@ -4,54 +4,24 @@
 #include <rive/shapes/rectangle.hpp>
 #include <rive/shapes/shape.hpp>
 #include "no_op_renderer.hpp"
+#include "rive_file_reader.hpp"
 #include <catch.hpp>
 #include <cstdio>
 
 TEST_CASE("file can be read", "[file]") {
-    FILE* fp = fopen("../../test/assets/two_artboards.riv", "r");
-    REQUIRE(fp != nullptr);
-
-    fseek(fp, 0, SEEK_END);
-    auto length = ftell(fp);
-    fseek(fp, 0, SEEK_SET);
-    uint8_t* bytes = new uint8_t[length];
-    REQUIRE(fread(bytes, 1, length, fp) == length);
-    auto reader = rive::BinaryReader(bytes, length);
-    rive::File* file = nullptr;
-    auto result = rive::File::import(reader, &file);
-
-    REQUIRE(result == rive::ImportResult::success);
-    REQUIRE(file != nullptr);
-    REQUIRE(file->artboard() != nullptr);
+    RiveFileReader reader("../../test/assets/two_artboards.riv");
 
     // Default artboard should be named Two.
-    REQUIRE(file->artboard()->name() == "Two");
+    REQUIRE(reader.file()->artboard()->name() == "Two");
 
     // There should be a second artboard named One.
-    REQUIRE(file->artboard("One") != nullptr);
-
-    delete file;
-    delete[] bytes;
+    REQUIRE(reader.file()->artboard("One") != nullptr);
 }
 
 TEST_CASE("file with animation can be read", "[file]") {
-    FILE* fp = fopen("../../test/assets/juice.riv", "r");
-    REQUIRE(fp != nullptr);
+    RiveFileReader reader("../../test/assets/juice.riv");
 
-    fseek(fp, 0, SEEK_END);
-    auto length = ftell(fp);
-    fseek(fp, 0, SEEK_SET);
-    uint8_t* bytes = new uint8_t[length];
-    REQUIRE(fread(bytes, 1, length, fp) == length);
-    auto reader = rive::BinaryReader(bytes, length);
-    rive::File* file = nullptr;
-    auto result = rive::File::import(reader, &file);
-
-    REQUIRE(result == rive::ImportResult::success);
-    REQUIRE(file != nullptr);
-    REQUIRE(file->artboard() != nullptr);
-
-    auto artboard = file->artboard();
+    auto artboard = reader.file()->artboard();
     REQUIRE(artboard->name() == "New Artboard");
 
     auto shin = artboard->find("shin_right");
@@ -70,29 +40,11 @@
     auto walkAnimation = artboard->animation("walk");
     REQUIRE(walkAnimation != nullptr);
     REQUIRE(walkAnimation->numKeyedObjects() == 22);
-
-    delete file;
-    delete[] bytes;
 }
 
 TEST_CASE("artboards can be counted and accessed via index or name", "[file]") {
-    FILE* fp = fopen("../../test/assets/dependency_test.riv", "r");
-    REQUIRE(fp != nullptr);
-
-    fseek(fp, 0, SEEK_END);
-    auto length = ftell(fp);
-    fseek(fp, 0, SEEK_SET);
-    uint8_t* bytes = new uint8_t[length];
-    REQUIRE(fread(bytes, 1, length, fp) == length);
-    auto reader = rive::BinaryReader(bytes, length);
-    rive::File* file = nullptr;
-    auto result = rive::File::import(reader, &file);
-
-    REQUIRE(result == rive::ImportResult::success);
-    REQUIRE(file != nullptr);
-
-    // The default artboard can be accessed
-    REQUIRE(file->artboard() != nullptr);
+    RiveFileReader reader("../../test/assets/dependency_test.riv");
+    auto file = reader.file();
 
     // The artboards caqn be counted
     REQUIRE(file->artboardCount() == 1);
@@ -102,9 +54,6 @@
 
     // Artboards can be accessed by name
     REQUIRE(file->artboard("Blue") != nullptr);
-
-    delete file;
-    delete[] bytes;
 }
 
 TEST_CASE("dependencies are as expected", "[file]") {
@@ -126,23 +75,9 @@
     //                   │ ┌──────────────┐
     //                   └▶│Rectangle Path│
     //                     └──────────────┘
-    FILE* fp = fopen("../../test/assets/dependency_test.riv", "r");
-    REQUIRE(fp != nullptr);
+    RiveFileReader reader("../../test/assets/dependency_test.riv");
 
-    fseek(fp, 0, SEEK_END);
-    auto length = ftell(fp);
-    fseek(fp, 0, SEEK_SET);
-    uint8_t* bytes = new uint8_t[length];
-    REQUIRE(fread(bytes, 1, length, fp) == length);
-    auto reader = rive::BinaryReader(bytes, length);
-    rive::File* file = nullptr;
-    auto result = rive::File::import(reader, &file);
-
-    REQUIRE(result == rive::ImportResult::success);
-    REQUIRE(file != nullptr);
-    REQUIRE(file->artboard() != nullptr);
-
-    auto artboard = file->artboard();
+    auto artboard = reader.file()->artboard();
     REQUIRE(artboard->name() == "Blue");
 
     auto nodeA = artboard->find<rive::Node>("A");
@@ -176,9 +111,6 @@
     auto world = shape->worldTransform();
     REQUIRE(world[4] == 39.203125f);
     REQUIRE(world[5] == 29.535156f);
-
-    delete file;
-    delete[] bytes;
 }
 
 // TODO:
@@ -202,4 +134,4 @@
 // setupFill/restoreFill and setupStroke/restoreStroke.
 
 // Draw will be called by C++ on the Shape, the Shape will call draw on the
-// fill/stroke (propagates to jsFill/jsStroke)
\ No newline at end of file
+// fill/stroke (propagates to jsFill/jsStroke)
diff --git a/test/ik_test.cpp b/test/ik_test.cpp
index 2db9ada..ab5eb14 100644
--- a/test/ik_test.cpp
+++ b/test/ik_test.cpp
@@ -1,30 +1,14 @@
-#include <rive/core/binary_reader.hpp>
-#include <rive/file.hpp>
 #include <rive/node.hpp>
 #include <rive/bones/bone.hpp>
 #include <rive/shapes/shape.hpp>
 #include "no_op_renderer.hpp"
+#include "rive_file_reader.hpp"
 #include "rive_testing.hpp"
 #include <cstdio>
 
 TEST_CASE("two bone ik places bones correctly", "[file]") {
-    FILE* fp = fopen("../../test/assets/two_bone_ik.riv", "r");
-    REQUIRE(fp != nullptr);
-
-    fseek(fp, 0, SEEK_END);
-    auto length = ftell(fp);
-    fseek(fp, 0, SEEK_SET);
-    uint8_t* bytes = new uint8_t[length];
-    REQUIRE(fread(bytes, 1, length, fp) == length);
-    auto reader = rive::BinaryReader(bytes, length);
-    rive::File* file = nullptr;
-    auto result = rive::File::import(reader, &file);
-
-    REQUIRE(result == rive::ImportResult::success);
-    REQUIRE(file != nullptr);
-    REQUIRE(file->artboard() != nullptr);
-
-    auto artboard = file->artboard();
+    RiveFileReader reader("../../test/assets/two_bone_ik.riv");
+    auto artboard = reader.file()->artboard();
 
     REQUIRE(artboard->find<rive::Shape>("circle a") != nullptr);
     auto circleA = artboard->find<rive::Shape>("circle a");
@@ -94,7 +78,4 @@
                                    0.882367908954620361328125f,
                                    240.1275634765625f,
                                    225.07647705078125f)));
-
-    delete file;
-    delete[] bytes;
-}
\ No newline at end of file
+}
diff --git a/test/rive_file_reader.hpp b/test/rive_file_reader.hpp
new file mode 100644
index 0000000..b18d062
--- /dev/null
+++ b/test/rive_file_reader.hpp
@@ -0,0 +1,39 @@
+#ifndef _RIVE_FILE_READER_HPP_
+#define _RIVE_FILE_READER_HPP_
+
+#include <rive/core/binary_reader.hpp>
+#include <rive/file.hpp>
+#include "rive_testing.hpp"
+
+class RiveFileReader {
+    rive::File* m_File = nullptr;
+    uint8_t* m_Bytes = nullptr;
+    rive::BinaryReader* m_Reader;
+
+public:
+    RiveFileReader(const char path[]) {
+        FILE* fp = fopen(path, "r");
+        REQUIRE(fp != nullptr);
+
+        fseek(fp, 0, SEEK_END);
+        const size_t length = ftell(fp);
+        fseek(fp, 0, SEEK_SET);
+        m_Bytes = new uint8_t[length];
+        REQUIRE(fread(m_Bytes, 1, length, fp) == length);
+        m_Reader = new rive::BinaryReader(m_Bytes, length);
+        auto result = rive::File::import(*m_Reader, &m_File);
+
+        REQUIRE(result == rive::ImportResult::success);
+        REQUIRE(m_File != nullptr);
+        REQUIRE(m_File->artboard() != nullptr);
+    }
+    ~RiveFileReader() {
+        delete m_File;
+        delete m_Reader;
+        delete[] m_Bytes;
+    }
+
+    rive::File* file() const { return m_File; }
+};
+
+#endif