From 211f8f1df439f27cf107589a51aebd8f91184430 Mon Sep 17 00:00:00 2001 From: Bram Date: Fri, 5 Jan 2024 13:51:06 +0100 Subject: [PATCH] Add Timeline framework The tests fail, but using test-driven development we will now build them functional --- src/Internal/Values/Timeline.elm | 114 ++++++++++++ tests/Test/Filter/Timeline.elm | 8 + tests/Test/Values/Timeline.elm | 303 +++++++++++++++++++++++++++++++ 3 files changed, 425 insertions(+) create mode 100644 src/Internal/Values/Timeline.elm create mode 100644 tests/Test/Values/Timeline.elm diff --git a/src/Internal/Values/Timeline.elm b/src/Internal/Values/Timeline.elm new file mode 100644 index 0000000..271a6dc --- /dev/null +++ b/src/Internal/Values/Timeline.elm @@ -0,0 +1,114 @@ +module Internal.Values.Timeline exposing + ( Batch, fromToken, fromSlice + , Timeline + , empty, singleton + , mostRecentEvents + , addSync, insert + ) + +{-| + + +# Timeline + +The Timeline data type represents a timeline in the Matrix room. The Matrix room +timeline is quite a complex data type, as it is constantly only partially known +by the Matrix client. This module exposes a data type that helps explore, track +and maintain this room state. + + +## Batch + +@docs Batch, fromToken, fromSlice + + +## Timeline + +@docs Timeline + + +## Create + +@docs empty, singleton + + +## Query + +@docs mostRecentEvents + + +## Manipulate + +@docs addSync, insert + +-} + +import Internal.Filter.Timeline as Filter exposing (Filter) + + +{-| The Timeline type represents the timeline state in a Matrix room. +-} +type Timeline + = Timeline + + +{-| A batch is a batch of events that is placed onto the Timeline. Functions +that require an insertion, generally require this data type. +-} +type Batch + = Batch + + +{-| When syncing a Matrix room to its most recent state, add the most recent +batch to the front of the Timeline. +-} +addSync : Batch -> Timeline -> Timeline +addSync _ timeline = + timeline + + +{-| Create a new empty timeline. +-} +empty : Timeline +empty = + Timeline + + +{-| Turn a single token into a batch. +-} +fromToken : String -> Batch +fromToken _ = + Batch + + +{-| Turn a slice of events into a batch. + +NOTE: `start` must generally be a value. If it is `Nothing`, then it is +connected until the start of the timeline. + +-} +fromSlice : { start : Maybe String, events : List String, filter : Filter, end : String } -> Batch +fromSlice _ = + Batch + + +{-| Insert a batch anywhere else in the timeline. +-} +insert : Batch -> Timeline -> Timeline +insert _ timeline = + timeline + + +{-| Under a given filter, find the most recent events. +-} +mostRecentEvents : Filter -> Timeline -> List String +mostRecentEvents _ _ = + [] + + +{-| Create a timeline with a single batch inserted. This batch is considered the +most recent batch, as if created by a sync. +-} +singleton : Batch -> Timeline +singleton b = + addSync b empty diff --git a/tests/Test/Filter/Timeline.elm b/tests/Test/Filter/Timeline.elm index 8b03f35..69b13dc 100644 --- a/tests/Test/Filter/Timeline.elm +++ b/tests/Test/Filter/Timeline.elm @@ -63,6 +63,14 @@ suite = (Filter.onlyTypes (head :: tail)) (Filter.onlySenders (head :: tail)) ) + , fuzz2 fuzzer + fuzzer + "Filter.and f1 f2 == pass iff f1 == f2 == pass" + (\filter1 filter2 -> + Expect.equal + (Filter.and filter1 filter2 == Filter.pass) + (filter1 == Filter.pass && filter2 == Filter.pass) + ) ] , describe "Event filters" [ fuzz TestEvent.fuzzer diff --git a/tests/Test/Values/Timeline.elm b/tests/Test/Values/Timeline.elm new file mode 100644 index 0000000..82f4629 --- /dev/null +++ b/tests/Test/Values/Timeline.elm @@ -0,0 +1,303 @@ +module Test.Values.Timeline exposing (..) + +import Fuzz exposing (Fuzzer) +import Internal.Filter.Timeline as Filter exposing (Filter) +import Internal.Values.Timeline as Timeline exposing (Batch, Timeline) +import Test exposing (..) +import Test.Filter.Timeline as TestFilter +import Expect + + +fuzzer : Fuzzer Timeline +fuzzer = + Fuzz.map2 + (\makers filter -> + case makers of + [] -> + Timeline.empty + + head :: tail -> + List.foldl + (\maker ( prevToken, timeline ) -> + case maker of + Sync start events end -> + ( end + , Timeline.addSync + (Timeline.fromSlice + { start = + start + |> Maybe.withDefault prevToken + |> Maybe.Just + , events = events + , filter = filter + , end = end + } + ) + timeline + ) + + Get start events efilter end -> + ( prevToken + , Timeline.insert + (Timeline.fromSlice + { start = start + , events = events + , filter = Filter.and filter efilter + , end = end + } + ) + timeline + ) + ) + (case head of + Sync start events end -> + ( end + , Timeline.addSync + (Timeline.fromSlice + { start = start + , events = events + , filter = filter + , end = end + } + ) + Timeline.empty + ) + + Get start events efilter end -> + ( end + , Timeline.addSync + (Timeline.fromSlice + { start = start + , events = events + , filter = Filter.and filter efilter + , end = end + } + ) + Timeline.empty + ) + ) + tail + |> Tuple.second + ) + (Fuzz.list fuzzerMaker) + TestFilter.fuzzer + + +fuzzerBatch : Fuzzer Batch +fuzzerBatch = + Fuzz.oneOf + [ Fuzz.map Timeline.fromToken Fuzz.string + , Fuzz.map4 + (\start events filter end -> + Timeline.fromSlice + { start = start + , events = events + , filter = filter + , end = end + } + ) + (Fuzz.maybe Fuzz.string) + (Fuzz.list Fuzz.string) + TestFilter.fuzzer + Fuzz.string + ] + + +type FuzzMaker + = Sync (Maybe String) (List String) String + | Get (Maybe String) (List String) Filter String + + +fuzzerMaker : Fuzzer FuzzMaker +fuzzerMaker = + Fuzz.frequency + [ ( 30, Fuzz.map (Sync Nothing []) Fuzz.string ) + , ( 10 + , Fuzz.map2 (Sync Nothing) + (Fuzz.listOfLengthBetween 1 32 Fuzz.string) + Fuzz.string + ) + , ( 1 + , Fuzz.map3 (\start events end -> Sync (Just start) events end) + Fuzz.string + (Fuzz.listOfLengthBetween 1 32 Fuzz.string) + Fuzz.string + ) + , ( 1 + , Fuzz.map4 Get + (Fuzz.maybe Fuzz.string) + (Fuzz.list Fuzz.string) + TestFilter.fuzzer + Fuzz.string + ) + ] + + +fuzzerForBatch : Fuzzer { start : String, events : List String, filter : Filter, end : String } +fuzzerForBatch = + Fuzz.map4 + (\start events filter end -> + { start = start, events = events, filter = filter, end = end } + ) + Fuzz.string + (Fuzz.list Fuzz.string) + TestFilter.fuzzer + Fuzz.string + + +suite : Test +suite = + describe "Timeline" + [ describe "Most recent events" + [ fuzz fuzzerForBatch "Singleton is most recent" + (\batch -> + { start = Just batch.start + , events = batch.events + , filter = batch.filter + , end = batch.end + } + |> Timeline.fromSlice + |> Timeline.singleton + |> Timeline.mostRecentEvents batch.filter + |> Expect.equal batch.events + ) + , fuzz2 fuzzerForBatch fuzzerForBatch "Double batch connects" + (\batch1 batch2 -> + [ { start = Just batch1.start + , events = batch1.events + , filter = batch1.filter + , end = batch2.start + } + , { start = Just batch2.start + , events = batch2.events + , filter = batch2.filter + , end = batch2.end + } + ] + |> List.map Timeline.fromSlice + |> List.foldl Timeline.addSync Timeline.empty + |> Timeline.mostRecentEvents (Filter.and batch1.filter batch2.filter) + |> (\outcome -> + if batch2.start == batch2.end then + Expect.equal [] outcome + else if batch1.start == batch2.start then + Expect.equal batch2.events outcome + else + Expect.equal + (List.append batch1.events batch2.events) + outcome + ) + ) + , fuzz2 fuzzerForBatch fuzzerForBatch "Disconnected double batch does not connect" + (\batch1 batch2 -> + [ { start = Just batch1.start + , events = batch1.events + , filter = batch1.filter + , end = batch1.start + } + , { start = Just batch2.start + , events = batch2.events + , filter = batch2.filter + , end = batch2.end + } + ] + |> List.map Timeline.fromSlice + |> List.foldl Timeline.addSync Timeline.empty + |> Timeline.mostRecentEvents (Filter.and batch1.filter batch2.filter) + |> (\outcome -> + if batch2.start == batch2.end then + Expect.equal [] outcome + else if batch1.start == batch2.start then + Expect.equal batch2.events outcome + else if batch1.end == batch2.start then + Expect.equal + (List.append batch1.events batch2.events) + outcome + else + Expect.equal batch2.events outcome + ) + ) + , fuzz + ( Fuzz.pair Fuzz.int (Fuzz.list Fuzz.string) + |> (\f -> Fuzz.triple f f f) + |> (\f -> Fuzz.triple f f f) + ) + "Connect 8 batches" + (\(((i1, e1), (i2, e2), (i3, e3)), ((i4, e4), (i5, e5), (i6, e6)), ((i7, e7), (i8, e8), (_, e9))) -> + [ ( i1 + , { start = Just <| String.fromInt 1 + , events = e1 + , filter = Filter.pass + , end = String.fromInt (1 + 1) + } + ) + , ( i2 + , { start = Just <| String.fromInt 2 + , events = e2 + , filter = Filter.pass + , end = String.fromInt (2 + 1) + } + ) + , ( i3 + , { start = Just <| String.fromInt 3 + , events = e3 + , filter = Filter.pass + , end = String.fromInt (3 + 1) + } + ) + , ( i4 + , { start = Just <| String.fromInt 4 + , events = e4 + , filter = Filter.pass + , end = String.fromInt (4 + 1) + } + ) + , ( i5 + , { start = Just <| String.fromInt 5 + , events = e5 + , filter = Filter.pass + , end = String.fromInt (5 + 1) + } + ) + , ( i6 + , { start = Just <| String.fromInt 6 + , events = e6 + , filter = Filter.pass + , end = String.fromInt (6 + 1) + } + ) + , ( i7 + , { start = Just <| String.fromInt 7 + , events = e7 + , filter = Filter.pass + , end = String.fromInt (7 + 1) + } + ) + , ( i8 + , { start = Just <| String.fromInt 8 + , events = e8 + , filter = Filter.pass + , end = String.fromInt (8 + 1) + } + ) + ] + |> List.sortBy Tuple.first + |> List.map Tuple.second + |> List.map Timeline.fromSlice + |> List.foldl + Timeline.insert + (Timeline.singleton + ( Timeline.fromSlice + { start = Just <| String.fromInt 9 + , events = e9 + , filter = Filter.pass + , end = String.fromInt (9 + 1) + } + ) + ) + |> Timeline.mostRecentEvents Filter.pass + |> Expect.equal + ( e1 ++ e2 ++ e3 ++ e4 ++ e5 ++ e6 ++ e7 ++ e8 ++ e9 ) + ) + ] + ]