-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #56 from jinaga/graph-notification
Correct bugs related to notifying a new graph
- Loading branch information
Showing
28 changed files
with
1,391 additions
and
335 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
using Jinaga.Facts; | ||
using Jinaga.Projections; | ||
using Jinaga.Serialization; | ||
using Jinaga.Services; | ||
using System.Collections.Immutable; | ||
using Xunit.Abstractions; | ||
|
||
namespace Jinaga.Store.SQLite.Test.Fakes; | ||
internal class FakeFeed | ||
{ | ||
public string Name { get; set; } | ||
public Fact[] Facts { get; set; } | ||
public int Delay { get; set; } | ||
} | ||
internal class FakeNetwork : INetwork | ||
{ | ||
private SerializerCache serializerCache = SerializerCache.Empty; | ||
private ITestOutputHelper output; | ||
private readonly List<FakeFeed> feeds = new(); | ||
private readonly Dictionary<FactReference, Fact> factByFactReference = new(); | ||
|
||
public FakeNetwork(ITestOutputHelper output) | ||
{ | ||
this.output = output; | ||
} | ||
|
||
public void AddFeed(string name, object[] facts, int delay = 0) | ||
{ | ||
var collector = new Collector(serializerCache); | ||
foreach (var fact in facts) | ||
{ | ||
collector.Serialize(fact); | ||
} | ||
serializerCache = collector.SerializerCache; | ||
var graph = collector.Graph; | ||
var serializedFacts = graph.FactReferences | ||
.Select(graph.GetFact) | ||
.ToArray(); | ||
foreach (var fact in serializedFacts) | ||
{ | ||
if (!factByFactReference.ContainsKey(fact.Reference)) | ||
{ | ||
factByFactReference.Add(fact.Reference, fact); | ||
} | ||
} | ||
feeds.Add(new FakeFeed | ||
{ | ||
Name = name, | ||
Facts = serializedFacts, | ||
Delay = delay | ||
}); | ||
} | ||
|
||
public Task<(FactGraph graph, UserProfile profile)> Login(CancellationToken cancellationToken) | ||
{ | ||
throw new NotImplementedException(); | ||
} | ||
|
||
public Task<ImmutableList<string>> Feeds(FactReferenceTuple givenTuple, Specification specification, CancellationToken cancellationToken) | ||
{ | ||
return Task.FromResult(feeds.Select(feed => feed.Name).ToImmutableList()); | ||
} | ||
|
||
public async Task<(ImmutableList<FactReference> references, string bookmark)> FetchFeed(string feed, string bookmark, CancellationToken cancellationToken) | ||
{ | ||
if (bookmark == "done") | ||
{ | ||
return (ImmutableList<FactReference>.Empty, "done"); | ||
} | ||
var fakeFeed = feeds.Single(f => f.Name == feed); | ||
var references = fakeFeed.Facts | ||
.Select(fact => fact.Reference) | ||
.ToImmutableList(); | ||
if (fakeFeed.Delay > 0) | ||
{ | ||
await Task.Delay(fakeFeed.Delay); | ||
} | ||
return (references, "done"); | ||
} | ||
|
||
public Task<FactGraph> Load(ImmutableList<FactReference> factReferences, CancellationToken cancellationToken) | ||
{ | ||
string references = string.Join(",\n", factReferences.Select(r => $" {r}")); | ||
output.WriteLine($"Load {factReferences.Count} facts:\n{references}"); | ||
var graph = FactGraph.Empty; | ||
|
||
foreach (var factReference in factReferences) | ||
{ | ||
graph = AddFact(graph, factReference); | ||
} | ||
|
||
return Task.FromResult(graph); | ||
} | ||
|
||
private FactGraph AddFact(FactGraph graph, FactReference factReference) | ||
{ | ||
var fact = factByFactReference[factReference]; | ||
foreach (var predecessor in fact.Predecessors) | ||
{ | ||
foreach (var predecessorReference in predecessor.AllReferences) | ||
{ | ||
graph = AddFact(graph, predecessorReference); | ||
} | ||
} | ||
graph = graph.Add(fact); | ||
return graph; | ||
} | ||
|
||
public Task Save(ImmutableList<Fact> facts, CancellationToken cancellationToken) | ||
{ | ||
throw new NotImplementedException(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
namespace Jinaga.Store.SQLite.Test.Model; | ||
|
||
|
||
[FactType("Corporate.Company")] | ||
record Company(string identifier); | ||
|
||
[FactType("Corporate.Office")] | ||
record Office(Company company, City city) | ||
{ | ||
public Condition IsClosed => new Condition(facts => | ||
facts.Any<OfficeClosure>(closure => closure.office == this) | ||
); | ||
} | ||
|
||
[FactType("Corporate.Office.Name")] | ||
record OfficeName(Office office, string value, OfficeName[] prior); | ||
|
||
|
||
[FactType("Corporate.Office.Closure")] | ||
record OfficeClosure(Office office, DateTime closureDate); | ||
|
||
[FactType("Corporate.Office.Reopening")] | ||
record OfficeReopening(OfficeClosure officeClosure, DateTime reopeningDate); | ||
|
||
[FactType("Corporate.City")] | ||
record City(string name); | ||
|
||
[FactType("Corporate.Headcount")] | ||
record Headcount(Office office, int value, Headcount[] prior) | ||
{ | ||
public Condition IsCurrent => new Condition(facts => !( | ||
from next in facts.OfType<Headcount>() | ||
where next.prior.Contains(this) | ||
select next | ||
).Any()); | ||
} | ||
|
||
[FactType("Corporate.Manager")] | ||
record Manager(Office office, int employeeNumber) | ||
{ | ||
public Condition IsTerminated => new Condition(facts => | ||
facts.OfType<ManagerTerminated>(termination => termination.Manager == this) | ||
.Any()); | ||
} | ||
|
||
[FactType("Corporate.Manager.Name")] | ||
record ManagerName(Manager manager, string value, ManagerName[] prior) | ||
{ | ||
public Condition IsCurrent => new Condition(facts => ! | ||
facts.OfType<ManagerName>(next => next.prior.Contains(this)) | ||
.Any()); | ||
} | ||
|
||
[FactType("Corporate.Manager.Terminated")] | ||
record ManagerTerminated(Manager Manager, DateTime terminationDate); |
201 changes: 201 additions & 0 deletions
201
Jinaga.Store.SQLite.Test/Observers/WatchFromNetworkTest.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
using FluentAssertions; | ||
using Jinaga.Storage; | ||
using Jinaga.Store.SQLite.Test.Fakes; | ||
using Jinaga.Store.SQLite.Test.Model; | ||
using Xunit.Abstractions; | ||
|
||
namespace Jinaga.Store.SQLite.Test.Observers; | ||
public class WatchFromNetworkTest | ||
{ | ||
private static string SQLitePath { get; } = Path.Combine( | ||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), | ||
"JinagaSQLiteTest", | ||
"WatchFromNetworkTest.db"); | ||
|
||
private ITestOutputHelper output; | ||
|
||
public WatchFromNetworkTest(ITestOutputHelper output) | ||
{ | ||
if (File.Exists(SQLitePath)) | ||
File.Delete(SQLitePath); | ||
|
||
this.output = output; | ||
} | ||
|
||
[Fact] | ||
public async Task Watch_EmptyUpstream() | ||
{ | ||
var j = GivenJinagaClient(new FakeNetwork(output)); | ||
|
||
var viewModel = new CompanyViewModel(); | ||
var watch = viewModel.Load(j, "contoso"); | ||
|
||
try | ||
{ | ||
await watch.Loaded; | ||
viewModel.Offices.Should().BeEmpty(); | ||
} | ||
finally | ||
{ | ||
watch.Stop(); | ||
} | ||
} | ||
|
||
[Fact] | ||
public async Task Watch_SingleUnnamedOffice() | ||
{ | ||
var network = new FakeNetwork(output); | ||
var contoso = new Company("contoso"); | ||
var dallas = new City("Dallas"); | ||
var dallasOffice = new Office(contoso, dallas); | ||
network.AddFeed("offices", new object[] | ||
{ | ||
dallasOffice | ||
}); | ||
|
||
var j = GivenJinagaClient(network); | ||
|
||
var viewModel = new CompanyViewModel(); | ||
var watch = viewModel.Load(j, "contoso"); | ||
|
||
try | ||
{ | ||
await watch.Loaded; | ||
viewModel.Offices.Should().ContainSingle().Which | ||
.Name.Should().BeNull(); | ||
} | ||
finally | ||
{ | ||
watch.Stop(); | ||
} | ||
} | ||
|
||
[Fact] | ||
public async Task Watch_SingleNamedOffice() | ||
{ | ||
var network = new FakeNetwork(output); | ||
var contoso = new Company("contoso"); | ||
var dallas = new City("Dallas"); | ||
var dallasOffice = new Office(contoso, dallas); | ||
var dallasOfficeName = new OfficeName(dallasOffice, "Dallas", new OfficeName[0]); | ||
network.AddFeed("offices", new object[] | ||
{ | ||
dallasOffice | ||
}, 1); | ||
network.AddFeed("officeNames", new object[] | ||
{ | ||
dallasOfficeName | ||
}, 1); | ||
|
||
var j = GivenJinagaClient(network); | ||
|
||
var viewModel = new CompanyViewModel(); | ||
var watch = viewModel.Load(j, "contoso"); | ||
|
||
try | ||
{ | ||
await watch.Loaded; | ||
viewModel.Offices.Should().ContainSingle().Which | ||
.Name.Should().Be("Dallas"); | ||
} | ||
finally | ||
{ | ||
watch.Stop(); | ||
} | ||
} | ||
|
||
[Fact] | ||
public async Task Watch_SingleOfficeThreeNames() | ||
{ | ||
var network = new FakeNetwork(output); | ||
var contoso = new Company("contoso"); | ||
var dallas = new City("Dallas"); | ||
var dallasOffice = new Office(contoso, dallas); | ||
var dallasOfficeName1 = new OfficeName(dallasOffice, "Dallas One", new OfficeName[0]); | ||
var dallasOfficeName2 = new OfficeName(dallasOffice, "Dallas Two", new OfficeName[] { dallasOfficeName1 }); | ||
var dallasOfficeName3 = new OfficeName(dallasOffice, "Dallas Three", new OfficeName[] { dallasOfficeName2 }); | ||
network.AddFeed("offices", new object[] | ||
{ | ||
dallasOffice | ||
}, 1); | ||
network.AddFeed("officeNames", new object[] | ||
{ | ||
dallasOfficeName1, | ||
dallasOfficeName2, | ||
dallasOfficeName3 | ||
}, 1); | ||
|
||
var j = GivenJinagaClient(network); | ||
|
||
var viewModel = new CompanyViewModel(); | ||
var watch = viewModel.Load(j, "contoso"); | ||
|
||
try | ||
{ | ||
await watch.Loaded; | ||
viewModel.Offices.Should().ContainSingle().Which | ||
.Name.Should().Be("Dallas Three"); | ||
} | ||
finally | ||
{ | ||
watch.Stop(); | ||
} | ||
} | ||
|
||
private static JinagaClient GivenJinagaClient(FakeNetwork network) | ||
{ | ||
return new JinagaClient(new SQLiteStore(SQLitePath), network); | ||
} | ||
|
||
private class OfficeViewModel | ||
{ | ||
public Office Office; | ||
public string Name; | ||
} | ||
|
||
private class CompanyViewModel | ||
{ | ||
public List<OfficeViewModel> Offices = new(); | ||
|
||
public IObserver Load(JinagaClient j, string identifier) | ||
{ | ||
var officesInCompany = Given<Company>.Match((company, facts) => | ||
from office in facts.OfType<Office>() | ||
where office.company == company | ||
where !office.IsClosed | ||
|
||
select new | ||
{ | ||
office, | ||
names = facts.Observable( | ||
from name in facts.OfType<OfficeName>() | ||
where name.office == office && | ||
!facts.Any<OfficeName>(next => next.prior.Contains(name)) | ||
select name.value | ||
) | ||
} | ||
); | ||
|
||
var company = new Company(identifier); | ||
var watch = j.Watch(officesInCompany, company, projection => | ||
{ | ||
var officeViewModel = new OfficeViewModel | ||
{ | ||
Office = projection.office | ||
}; | ||
Offices.Add(officeViewModel); | ||
|
||
projection.names.OnAdded(name => | ||
{ | ||
officeViewModel.Name = name; | ||
}); | ||
|
||
return () => | ||
{ | ||
Offices.Remove(officeViewModel); | ||
}; | ||
}); | ||
return watch; | ||
} | ||
} | ||
} |
Oops, something went wrong.