packages feed

armor 0.1 → 0.2

raw patch · 8 files changed

+203/−40 lines, 8 filesdep +hashabledep ~aesondep ~basedep ~bytestringPVP ok

version bump matches the API change (PVP)

Dependencies added: hashable

Dependency ranges changed: aeson, base, bytestring, containers, lens

API changes (from Hackage documentation)

+ Armor: GoldenTest :: a -> String -> String -> APrism' ByteString a -> Version a -> GoldenTest a
+ Armor: SaveAndTest :: ArmorMode
+ Armor: SaveOnly :: ArmorMode
+ Armor: TestOnly :: ArmorMode
+ Armor: [gtPrism] :: GoldenTest a -> APrism' ByteString a
+ Armor: [gtSerializationName] :: GoldenTest a -> String
+ Armor: [gtTestVal] :: GoldenTest a -> a
+ Armor: [gtValName] :: GoldenTest a -> String
+ Armor: [gtVersion] :: GoldenTest a -> Version a
+ Armor: data ArmorMode
+ Armor: data GoldenTest a
+ Armor: goldenFilePath :: Typeable a => GoldenTest a -> FilePath
+ Armor: testSerialization :: forall a. (Eq a, Show a, Typeable a, Armored a) => ArmorConfig -> (GoldenTest a -> FilePath) -> String -> (String, APrism' ByteString a) -> a -> Assertion

Files

ChangeLog.md view
@@ -1,5 +1,22 @@ # Revision history for armor -## 0.1      -- YYYY-mm-dd+## 0.2      -- 2021-05-06++* Widen version bounds+* Support and test through GHC 9+* Expose more customizable test function+* Change default mechanics of FilePath generation++WARNING: Depending on how you use armor this is a potentially+backwards-incompatible change!  As a precaution we are doing a major version+bump even though the change would only technically need a C bump.++The best way to upgrade your app is to upgrade armor in a commit by itself+with no other changes to your app.  That way, if there armor has test case+failures, you know that they are innocuous because no part of the rest of your+app changed functionaly.  So you can simple delete the failing golds,+regenerate them, and check the new ones into source control.++## 0.1      -- 2018-03-14  * First version. Released on an unsuspecting world.
+ README.md view
@@ -0,0 +1,42 @@+# Armor++[![Build Status](https://travis-ci.org/mightybyte/armor.svg?branch=master)](https://travis-ci.org/mightybyte/armor)++Armor yourself against backwards-incompatible serializations once and for all.++See the+[ChangeLog](https://github.com/mightybyte/armor/blob/master/ChangeLog.md) for+information about changes between versions.++## Motivation++As almost everyone with significant experience managing production software+systems knows, backwards compatibility is incredibly important for any data that+is persisted by an application. If you make a change to a data structure that is+not backwards compatible with the existing serialized formats, your app will+break as soon as it encounters the existing format. Even if you have 100% test+coverage, your tests still might not catch this problem because it's not a+problem with your app at any single point in time, but a problem with how your+app evolves over time.++More subtly, if you deploy a backwards incompatible migration, your app may+persist some data in the new format before it crashes on the old format. This+can leave your system in the horrible state where not only will it not work with+the new code, but rolling back to the old code will also break because the old+code doesn't support the new serialized format! You have two incomptable+serializations active at the same time!++Proper migration systems can reduce the chances of this problem occurring, but+if your system has any kind of queueing system or message bus, your migrations+might not be applied to in-flight messages. Clearly we need something to help us+protect against this problem.  Enter `armor`.++For an overview of how to use this package, check out the [literate Haskell+tutorial in the test suite](test/AppA.lhs).++## Credits++Inspiration for this package came from [Soostone's safecopy-hunit package](https://github.com/Soostone/safecopy-hunit).++Details were refined in production at [Formation](http://formation.ai/)+(previously Takt).
armor.cabal view
@@ -1,6 +1,6 @@ name:                armor-version:             0.1-synopsis:            Armor data structures against serialization backwards compatibility problems+version:             0.2+synopsis:            Prevent serialization backwards compatibility problems using golden tests description:         Tests the serialization backwards compatibility of data types by storing                      serialized representations in .test files to be checked into your project's                      version control.@@ -11,16 +11,24 @@ copyright:           Doug Beardsley, Formation Inc. homepage:            https://github.com/mightybyte/armor bug-reports:         https://github.com/mightybyte/armor/issues-category:            Data+category:            Data,Testing build-type:          Simple extra-source-files:  ChangeLog.md cabal-version:       >=1.10 tested-with:   GHC==7.10.3,   GHC==8.0.2,-  GHC==8.2.1,-  GHC==8.4.1+  GHC==8.2.2,+  GHC==8.4.4,+  GHC==8.6.3,+  GHC==8.8.3,+  GHC==8.10.1,+  GHC==9.0.1 +extra-source-files:+  default.nix+  README.md+ Source-repository head   Type:     git   Location: https://github.com/mightybyte/armor.git@@ -33,12 +41,13 @@   ghc-options: -Wall   build-depends:     HUnit      >= 1.5  && < 1.7,-    base       >= 4.6  && < 4.12,-    bytestring >= 0.10 && < 0.11,-    containers >= 0.5  && < 0.6,+    base       >= 4.6  && < 4.16,+    bytestring >= 0.10 && < 0.12,+    containers >= 0.5  && < 0.7,     directory  >= 1.2  && < 1.4,     filepath   >= 1.4  && < 1.5,-    lens       >= 4.16 && < 4.17+    hashable   >= 1.3  && < 1.4,+    lens       >= 4.16 && < 5.1    default-language:    Haskell2010 @@ -56,13 +65,13 @@   ghc-options: -Wall   build-depends:     HUnit,-    aeson        >= 1.0 && < 1.3,+    aeson        >= 1.0 && < 1.6,     armor,     base,     bytestring,     containers,     directory,-    hspec        >= 2.4 && < 2.5,+    hspec        >= 2.4 && < 2.8,     lens,     text         >= 1.2 && < 1.3 
+ default.nix view
@@ -0,0 +1,63 @@+{ compiler ? "ghc8104"+, rev      ? "282473158cf8fafb736ed2a09f328be8b7ed2efa"+, sha256   ? "1nz2h6dmb6s5zcilb39l116y4dmp1c6w9n110g4dilm6v01linj2"+, pkgs     ?+    import (builtins.fetchTarball {+      url    = "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz";+      inherit sha256; }) {+      config.allowBroken = false;+      config.allowUnfree = true;+    }+}:+let gitignoreSrc = import (pkgs.fetchFromGitHub {+      owner = "hercules-ci";+      repo = "gitignore";+      rev = "2ced4519f865341adcb143c5d668f955a2cb997f";+      sha256 = "0fc5bgv9syfcblp23y05kkfnpgh3gssz6vn24frs8dzw39algk2z";+    }) {};++in+pkgs.haskell.packages.${compiler}.developPackage {+  name = builtins.baseNameOf ./.;+  root = gitignoreSrc.gitignoreSource ./.;++  overrides = self: super: with pkgs.haskell.lib; {+    # Don't run a package's test suite+    # foo = dontCheck super.foo;+    #+    # Don't enforce package's version constraints+    # bar = doJailbreak super.bar;+    #+    # Get a specific hackage version straight from hackage. Unlike the above+    # callHackage approach, this will always succeed if the version is on+    # hackage. The downside is that you have to specify the hash manually.+    # aeson = callHackageDirect {+    #   pkg = "aeson";+    #   ver = "1.4.2.0";+    #   sha256 = "0qcczw3l596knj9s4ha07wjspd9wkva0jv4734sv3z3vdad5piqh";+    # } {};+    #+    # To discover more functions that can be used to modify haskell+    # packages, run "nix-repl", type "pkgs.haskell.lib.", then hit+    # <TAB> to get a tab-completed list of functions.+  };+  source-overrides = {+    # Use a specific hackage version using callHackage. Only works if the+    # version you want is in the version of all-cabal-hashes that you have.+    # bytestring = "0.10.8.1";+    #+    # Use a particular commit from github+    # parsec = pkgs.fetchFromGitHub+    #   { owner = "hvr";+    #     repo = "parsec";+    #     rev = "c22d391c046ef075a6c771d05c612505ec2cd0c3";+    #     sha256 = "0phar79fky4yzv4hq28py18i4iw779gp5n327xx76mrj7yj87id3";+    #   };+  };+  modifier = drv: pkgs.haskell.lib.overrideCabal drv (attrs: {+    buildTools = (attrs.buildTools or []) ++ [+      pkgs.haskell.packages.${compiler}.cabal-install+      pkgs.haskell.packages.${compiler}.ghcid+    ];+  });+}
src/Armor.hs view
@@ -5,10 +5,14 @@ module Armor   ( Version(..)   , Armored(..)+  , ArmorMode(..)   , ArmorConfig(..)   , defArmorConfig   , testArmor   , testArmorMany+  , testSerialization+  , GoldenTest(..)+  , goldenFilePath   ) where  ------------------------------------------------------------------------------@@ -16,12 +20,15 @@ import           Control.Monad import           Data.ByteString (ByteString) import qualified Data.ByteString as B+import           Data.Char+import           Data.Hashable import           Data.Map        (Map) import qualified Data.Map        as M import           Data.Typeable #if !MIN_VERSION_base(4,8,0) import           Data.Word #endif+import           Numeric import           System.Directory import           System.FilePath import           Test.HUnit.Base@@ -103,11 +110,11 @@ testArmor ac valId val =     TestList [ testIt s | s <- M.toList serializations ]   where-    testIt s = test (testSerialization ac valId val s)+    testIt s = test (testSerialization ac goldenFilePath valId s val)   --------------------------------------------------------------------------------- Same as 'testArmor', but more convenient for testing several values of the+-- | Same as 'testArmor', but more convenient for testing several values of the -- same type. testArmorMany     :: (Eq a, Show a, Typeable a, Armored a)@@ -120,31 +127,37 @@   ------------------------------------------------------------------------------+-- | Lower level assertion function that works for a wider array of test+-- frameworks. testSerialization     :: forall a. (Eq a, Show a, Typeable a, Armored a)     => ArmorConfig+    -> (GoldenTest a -> FilePath)+    -- ^ Customizable location where the serializations will be stored. We+    -- recommend 'goldenFilePath' as a standard out-of-the-box scheme.     -> String-    -> a     -> (String, APrism' ByteString a)+    -> a     -> Assertion-testSerialization ac valId val s@(_,p) = do-    let d = getVersionDir ac val s-        f = getVersionFilename valId curVer-        fp = d </> f-    when (acArmorMode ac /= TestOnly) $ do-      createDirectoryIfMissing True d-      fileExists <- doesFileExist fp-      when (not fileExists) $-        B.writeFile fp (review (clonePrism p) val)-    when (acArmorMode ac /= SaveOnly) $ do-      mapM_ (assertVersionParses d . Version) vs+testSerialization ac makeFilePath valName (sname,p) val = do+    ensureTestFileExists+    when (acArmorMode ac /= SaveOnly) $+      mapM_ (assertVersionParses . Version) vs   where+    makeGT = GoldenTest val valName sname p     curVer :: Version a     curVer = version     vs = reverse [maybe 0 (unVersion curVer -) (acNumVersions ac) .. unVersion curVer]-    assertVersionParses d ver = do-        let f = getVersionFilename valId ver-            fp = d </> f+    ensureTestFileExists = do+      let fp = acStoreDir ac </> makeFilePath (makeGT curVer)+          d = dropFileName fp+      when (acArmorMode ac /= TestOnly) $ do+        createDirectoryIfMissing True d+        fileExists <- doesFileExist fp+        when (not fileExists) $+          B.writeFile fp (review (clonePrism p) val)+    assertVersionParses ver = do+        let fp = acStoreDir ac </> makeFilePath (makeGT ver)         exists <- doesFileExist fp         if exists           then do bs <- B.readFile fp@@ -155,12 +168,31 @@                     Just v -> assertEqual ("File parsed but values didn't match: " ++ fp) val v           else putStrLn $ "\nSkipping missing file " ++ fp - -------------------------------------------------------------------------------getVersionFilename :: String -> Version a -> String-getVersionFilename valId ver = printf "%s-%03d.test" valId (unVersion ver)-+-- | Data structure that holds all the values needed for a golden test+data GoldenTest a = GoldenTest+  { gtTestVal :: a+  , gtValName :: String+  , gtSerializationName :: String+  , gtPrism :: APrism' ByteString a+  , gtVersion :: Version a+  }  -------------------------------------------------------------------------------getVersionDir :: Typeable a => ArmorConfig -> a -> (FilePath, t) -> FilePath-getVersionDir ac val (nm,_) = acStoreDir ac </> show (typeOf val) </> nm+-- | Constructs the FilePath where the serialization will be stored (relative to+-- the base directory defined in ArmorConfig).+--+-- This function uses typeOf as a part of the directory hierarchy to+-- disambiguate tests for different data types. typeOf can contain single+-- quotes, spaces, and parenthesis in the case of type constructors that have+-- type variables so we only take the first alphanumeric characters so that the+-- paths will be meaningful to humans and then add four characters of the type's+-- hash for disambiguation.+goldenFilePath :: Typeable a => GoldenTest a -> FilePath+goldenFilePath gt =+    (concat [takeWhile isAlpha ty, "-", h]) </>+    gtSerializationName gt </>+    printf "%s-%03d.test" (gtValName gt) (unVersion $ gtVersion gt)+  where+    ty = show $ typeOf $ gtTestVal gt+    h = take 4 $ showHex (abs $ hash ty) ""
test/AppA.lhs view
@@ -69,7 +69,7 @@  This tutorial is a part of the armor test suite, and since we don't want armor to depend on any specific serialization packages we're using Show as an example-of how armor supports any number of serialiaztions.+of how armor supports any number of serializations.  A serialization is simply a pair of a serialization function that converts your data type to ByteString and a deserialization function that converts a@@ -88,4 +88,4 @@ Once you have defined your `Armored` instances, the next step is to define your tests.  To see an example of that go here: -https://github.com/TaktInc/armor/blob/master/test/TestAppA.lhs+https://github.com/mightybyte/armor/blob/master/test/TestAppA.lhs
test/AppB.lhs view
@@ -34,7 +34,7 @@ > instance FromJSON EmployeeLevel > instance ToJSON EmployeeLevel -We update the `Armored` instance to version 1. If you forget to updated the+We update the `Armored` instance to version 1. If you forget to update the version, the armor tests should still fail because the existing version 0 serialization files will not be overwritten. @@ -57,4 +57,4 @@  Now go to the TestAppB module to see how we update the tests for the new field. -https://github.com/TaktInc/armor/blob/master/test/TestAppB.lhs+https://github.com/mightybyte/armor/blob/master/test/TestAppB.lhs
test/TestAppA.lhs view
@@ -24,4 +24,4 @@ that they can be correctly parsed.  Eventually you'll need to change the data type in some way.  See this file for a case study of how that might play out: -https://github.com/TaktInc/armor/blob/master/test/AppB.lhs+https://github.com/mightybyte/armor/blob/master/test/AppB.lhs