From 9b2d3c03b0ed4e0d189ff9fd99bd8c4ca9b626ba Mon Sep 17 00:00:00 2001 From: smsunarto Date: Wed, 8 Nov 2023 19:41:13 -0800 Subject: [PATCH 01/14] refactor: upstream config and refactor NewWorld --- cardinal/LICENSE | 208 ------------------ cardinal/cardinal_test.go | 11 +- cardinal/config.go | 28 +++ cardinal/ecs/chain_recover_test.go | 6 +- .../metadata/component_metadata_test.go | 7 +- cardinal/ecs/ecb/ecb_test.go | 4 +- cardinal/ecs/ecb/read_only_test.go | 4 +- cardinal/ecs/ecs_test.go | 31 +-- cardinal/ecs/filter/filter_test.go | 13 +- cardinal/ecs/log/log_test.go | 3 +- cardinal/ecs/message/message_test.go | 25 ++- cardinal/ecs/message_test.go | 3 +- cardinal/ecs/mock_world.go | 63 ------ cardinal/ecs/persona_test.go | 9 +- cardinal/ecs/query_test.go | 3 +- cardinal/ecs/search_test.go | 3 +- cardinal/ecs/tick_test.go | 5 +- cardinal/ecs/world_test.go | 24 +- cardinal/events/events_test.go | 7 +- cardinal/evm/server_test.go | 18 +- cardinal/message.go | 2 +- cardinal/option.go | 43 ++++ cardinal/server/server_test.go | 58 +++-- cardinal/testing.go | 6 +- cardinal/testutils/test_utils.go | 19 +- cardinal/testutils/world.go | 24 ++ cardinal/world.go | 89 ++++---- 27 files changed, 283 insertions(+), 433 deletions(-) delete mode 100644 cardinal/LICENSE create mode 100644 cardinal/config.go delete mode 100644 cardinal/ecs/mock_world.go create mode 100644 cardinal/testutils/world.go diff --git a/cardinal/LICENSE b/cardinal/LICENSE deleted file mode 100644 index 779b35347..000000000 --- a/cardinal/LICENSE +++ /dev/null @@ -1,208 +0,0 @@ -Business Source License 1.1 - -License text copyright © 2023 MariaDB plc, All Rights Reserved. -“Business Source License” is a trademark of MariaDB plc. - ------------------------------------------------------------------------------ - -Parameters - -Licensor: Argus World Labs, Inc. - -Licensed Work: World Engine - The Licensed Work is (c) 2023 Argus World Labs, Inc. - -Additional Use Grant: - -Change Date: 2027-03-06 - -Change License: GNU Lesser General Public License v3.0 or later - -For information about alternative licensing arrangements for the Software, -please visit: https://mariadb.com/products/mariadb-enterprise - ------------------------------------------------------------------------------ - -Terms - -The Licensor hereby grants you the right to copy, modify, create derivative -works, redistribute, and make non-production use of the Licensed Work. The -Licensor may make an Additional Use Grant, above, permitting limited -production use. - -Effective on the Change Date, or the fourth anniversary of the first publicly -available distribution of a specific version of the Licensed Work under this -License, whichever comes first, the Licensor hereby grants you rights under -the terms of the Change License, and the rights granted in the paragraph -above terminate. - -If your use of the Licensed Work does not comply with the requirements -currently in effect as described in this License, you must purchase a -commercial license from the Licensor, its affiliated entities, or authorized -resellers, or you must refrain from using the Licensed Work. - -All copies of the original and modified Licensed Work, and derivative works -of the Licensed Work, are subject to this License. This License applies -separately for each version of the Licensed Work and the Change Date may vary -for each version of the Licensed Work released by Licensor. - -You must conspicuously display this License on each original or modified copy -of the Licensed Work. If you receive the Licensed Work in original or -modified form from a third party, the terms and conditions set forth in this -License apply to your use of that work. - -Any use of the Licensed Work in violation of this License will automatically -terminate your rights under this License for the current and all other -versions of the Licensed Work. - -This License does not grant you any right in any trademark or logo of -Licensor or its affiliates (provided that you may use a trademark or logo of -Licensor as expressly required by this License). - -TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON -AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, -EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND -TITLE. - -MariaDB hereby grants you permission to use this License’s text to license -your works, and to refer to it using the trademark “Business Source License”, -as long as you comply with the Covenants of Licensor below. - ------------------------------------------------------------------------------ - -Covenants of Licensor - -In consideration of the right to use this License’s text and the “Business -Source License” name and trademark, Licensor covenants to MariaDB, and to all -other recipients of the licensed work to be provided by Licensor: - -1. To specify as the Change License the GPL Version 2.0 or any later version, - or a license that is compatible with GPL Version 2.0 or a later version, - where “compatible” means that software provided under the Change License can - be included in a program with software provided under GPL Version 2.0 or a - later version. Licensor may specify additional Change Licenses without - limitation. - -2. To either: (a) specify an additional grant of rights to use that does not - impose any additional restriction on the right granted in this License, as - the Additional Use Grant; or (b) insert the text “None”. - -3. To specify a Change Date. - -4. Not to modify this License in any other way. - ------------------------------------------------------------------------------ - -Notice - -The Business Source License (this document, or the “License”) is not an Open -Source license. However, the Licensed Work will eventually be made available -under an Open Source License, as stated in this License. - -For more information on the use of the Business Source License for MariaDB -products, please visit the MariaDB Business Source License FAQ at -https://mariadb.com/bsl-faq-mariadb. - -For more information on the use of the Business Source License generally, -please visit the Adopting and Developing Business Source License FAQ at -https://mariadb.com/bsl-faq-adopting. - ------------------------------------------------------------------------------ - -MIT License - -Copyright (c) 2022 Yota Hamada - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -MIT License - -Copyright (c) 2020 Thomas Gillen - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -This repository licensed under: - -MIT License - -Copyright (c) 2021 Artem Sedykh - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -Repository github.com/wfranczyk/ento licensed under: - -MIT License - -Copyright (c) 2021 Wojciech Franczyk - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -The fire particles images was made by Kenney (kenney.nl) and licensed under: - -CC0 1.0 Universal -https://creativecommons.org/publicdomain/zero/1.0/ diff --git a/cardinal/cardinal_test.go b/cardinal/cardinal_test.go index 6be6615fb..31fee972e 100644 --- a/cardinal/cardinal_test.go +++ b/cardinal/cardinal_test.go @@ -22,8 +22,8 @@ func (Foo) Name() string { return "foo" } func TestNewWorld(t *testing.T) { // should fail, this test should generate a compile error if the function signature changes. - _, err := cardinal.NewWorld("", "", cardinal.WithNamespace("testnamespace")) - assert.Assert(t, err != nil) + _, err := cardinal.NewWorld(cardinal.WithNamespace("testnamespace")) + assert.NilError(t, err) } func TestCanQueryInsideSystem(t *testing.T) { @@ -59,9 +59,8 @@ func TestShutdownViaSignal(t *testing.T) { // If this test is frozen then it failed to shut down, create a failure with panic. var wg sync.WaitGroup testutils.SetTestTimeout(t, 10*time.Second) - world, err := cardinal.NewMockWorld(cardinal.WithCORS()) + world := testutils.NewTestWorld(t, cardinal.WithCORS()) assert.NilError(t, cardinal.RegisterComponent[Foo](world)) - assert.NilError(t, err) wantNumOfEntities := 10 world.Init(func(worldCtx cardinal.WorldContext) error { _, err := cardinal.CreateMany(worldCtx, wantNumOfEntities/2, Foo{}) @@ -72,7 +71,7 @@ func TestShutdownViaSignal(t *testing.T) { }) wg.Add(1) go func() { - err = world.StartGame() + err := world.StartGame() assert.NilError(t, err) wg.Done() }() @@ -81,7 +80,7 @@ func TestShutdownViaSignal(t *testing.T) { time.Sleep(500 * time.Millisecond) } wCtx := cardinal.TestingWorldToWorldContext(world) - _, err = cardinal.CreateMany(wCtx, wantNumOfEntities/2, Foo{}) + _, err := cardinal.CreateMany(wCtx, wantNumOfEntities/2, Foo{}) assert.NilError(t, err) // test CORS with cardinal client := &http.Client{} diff --git a/cardinal/config.go b/cardinal/config.go new file mode 100644 index 000000000..f18df0cbf --- /dev/null +++ b/cardinal/config.go @@ -0,0 +1,28 @@ +package cardinal + +import "os" + +type WorldConfig struct { + RedisAddress string + RedisPassword string + CardinalWorldId string + CardinalPort string + CardinalDeployMode string +} + +func GetWorldConfig() WorldConfig { + return WorldConfig{ + RedisAddress: getEnv("REDIS_ADDRESS", "localhost:6379"), + RedisPassword: getEnv("REDIS_PASSWORD", ""), + CardinalWorldId: getEnv("CARDINAL_WORLD_ID", "world"), + CardinalPort: getEnv("CARDINAL_PORT", "3333"), + CardinalDeployMode: getEnv("CARDINAL_DEPLOY_MODE", "development"), + } +} + +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} diff --git a/cardinal/ecs/chain_recover_test.go b/cardinal/ecs/chain_recover_test.go index 3afa3cc81..f84f8fc17 100644 --- a/cardinal/ecs/chain_recover_test.go +++ b/cardinal/ecs/chain_recover_test.go @@ -3,7 +3,9 @@ package ecs_test import ( "context" "encoding/binary" + "pkg.world.dev/world-engine/cardinal" "pkg.world.dev/world-engine/cardinal/ecs/message" + "pkg.world.dev/world-engine/cardinal/testutils" "sort" "testing" @@ -91,7 +93,7 @@ func TestWorld_RecoverFromChain(t *testing.T) { // setup world and transactions ctx := context.Background() adapter := &DummyAdapter{txs: make(map[uint64][]*types.Transaction, 0)} - w := ecs.NewTestWorld(t, ecs.WithAdapter(adapter)) + w := testutils.NewTestWorld(t, cardinal.WithAdapter(adapter)).Instance() sendEnergyTx := ecs.NewMessageType[SendEnergyMsg, SendEnergyResult]("send_energy") err := w.RegisterMessages(sendEnergyTx) assert.NilError(t, err) @@ -146,7 +148,7 @@ func generateRandomTransaction(t *testing.T, ns string, msg message.Message) *si func TestWorld_RecoverShouldErrorIfTickExists(t *testing.T) { ctx := context.Background() adapter := &DummyAdapter{} - w := ecs.NewTestWorld(t, ecs.WithAdapter(adapter)) + w := testutils.NewTestWorld(t, cardinal.WithAdapter(adapter)).Instance() assert.NilError(t, w.LoadGameState()) assert.NilError(t, w.Tick(ctx)) diff --git a/cardinal/ecs/component/metadata/component_metadata_test.go b/cardinal/ecs/component/metadata/component_metadata_test.go index 7eb7fa795..d32de9c2b 100644 --- a/cardinal/ecs/component/metadata/component_metadata_test.go +++ b/cardinal/ecs/component/metadata/component_metadata_test.go @@ -1,6 +1,7 @@ package metadata_test import ( + "pkg.world.dev/world-engine/cardinal/testutils" "testing" "gotest.tools/v3/assert" @@ -39,7 +40,7 @@ func TestComponentInterfaceSignature(t *testing.T) { } func TestComponents(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() ecs.MustRegisterComponent[ComponentDataA](world) ecs.MustRegisterComponent[ComponentDataB](world) @@ -141,7 +142,7 @@ func (notFoundComp) Name() string { } func TestErrorWhenAccessingComponentNotOnEntity(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() ecs.MustRegisterComponent[foundComp](world) ecs.MustRegisterComponent[notFoundComp](world) @@ -161,7 +162,7 @@ func (ValueComponent) Name() string { } func TestMultipleCallsToCreateSupported(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[ValueComponent](world)) wCtx := ecs.NewWorldContext(world) diff --git a/cardinal/ecs/ecb/ecb_test.go b/cardinal/ecs/ecb/ecb_test.go index 4540d380e..068fcb8e8 100644 --- a/cardinal/ecs/ecb/ecb_test.go +++ b/cardinal/ecs/ecb/ecb_test.go @@ -1,6 +1,8 @@ package ecb_test import ( + "pkg.world.dev/world-engine/cardinal" + "pkg.world.dev/world-engine/cardinal/testutils" "testing" "gotest.tools/v3/assert" @@ -290,7 +292,7 @@ func (Power) Name() string { func TestStorageCanBeUsedInQueries(t *testing.T) { manager := newCmdBufferForTest(t) - world := ecs.NewTestWorld(t, ecs.WithStoreManager(manager)) + world := testutils.NewTestWorld(t, cardinal.WithStoreManager(manager)).Instance() assert.NilError(t, ecs.RegisterComponent[Health](world)) assert.NilError(t, ecs.RegisterComponent[Power](world)) assert.NilError(t, world.LoadGameState()) diff --git a/cardinal/ecs/ecb/read_only_test.go b/cardinal/ecs/ecb/read_only_test.go index fca69b1ae..24bd40668 100644 --- a/cardinal/ecs/ecb/read_only_test.go +++ b/cardinal/ecs/ecb/read_only_test.go @@ -1,6 +1,8 @@ package ecb_test import ( + "pkg.world.dev/world-engine/cardinal" + "pkg.world.dev/world-engine/cardinal/testutils" "testing" "gotest.tools/v3/assert" @@ -174,7 +176,7 @@ func TestReadOnly_ArchetypeCount(t *testing.T) { func TestReadOnly_SearchFrom(t *testing.T) { manager := newCmdBufferForTest(t) - world := ecs.NewTestWorld(t, ecs.WithStoreManager(manager)) + world := testutils.NewTestWorld(t, cardinal.WithStoreManager(manager)).Instance() assert.NilError(t, ecs.RegisterComponent[Health](world)) assert.NilError(t, ecs.RegisterComponent[Power](world)) assert.NilError(t, world.LoadGameState()) diff --git a/cardinal/ecs/ecs_test.go b/cardinal/ecs/ecs_test.go index b23f9af54..ae2541539 100644 --- a/cardinal/ecs/ecs_test.go +++ b/cardinal/ecs/ecs_test.go @@ -3,6 +3,7 @@ package ecs_test import ( "context" "errors" + "pkg.world.dev/world-engine/cardinal/testutils" "testing" "gotest.tools/v3/assert" @@ -63,7 +64,7 @@ var ( ) func TestECS(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[EnergyComponent](world)) assert.NilError(t, ecs.RegisterComponent[OwnableComponent](world)) @@ -117,7 +118,7 @@ func (Vel) Name() string { } func TestVelocitySimulation(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() // These components are a mix of concrete types and pointer types to make sure they both work assert.NilError(t, ecs.RegisterComponent[Pos](world)) @@ -159,7 +160,7 @@ func (Owner) Name() string { } func TestCanSetDefaultValue(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() wantOwner := Owner{"Jeff"} @@ -191,7 +192,7 @@ func (Tuple) Name() string { } func TestCanRemoveEntity(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[Tuple](world)) assert.NilError(t, world.LoadGameState()) @@ -255,7 +256,7 @@ func (CountComponent) Name() string { } func TestCanRemoveEntriesDuringCallToEach(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[CountComponent](world)) assert.NilError(t, world.LoadGameState()) @@ -302,7 +303,7 @@ func TestCanRemoveEntriesDuringCallToEach(t *testing.T) { } func TestAddingAComponentThatAlreadyExistsIsError(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[EnergyComponent](world)) assert.NilError(t, world.LoadGameState()) @@ -331,7 +332,7 @@ func (WeaponEnergy) Name() string { } func TestRemovingAMissingComponentIsError(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[ReactorEnergy](world)) assert.NilError(t, ecs.RegisterComponent[WeaponEnergy](world)) assert.NilError(t, world.LoadGameState()) @@ -354,7 +355,7 @@ func (Bar) Name() string { } func TestVerifyAutomaticCreationOfArchetypesWorks(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[Foo](world)) assert.NilError(t, ecs.RegisterComponent[Bar](world)) @@ -404,7 +405,7 @@ func (Gamma) Name() string { } func TestEntriesCanChangeTheirArchetype(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[Alpha](world)) assert.NilError(t, ecs.RegisterComponent[Beta](world)) assert.NilError(t, ecs.RegisterComponent[Gamma](world)) @@ -479,7 +480,7 @@ func (e EnergyComponentBeta) Name() string { } func TestCannotSetComponentThatDoesNotBelongToEntity(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[EnergyComponentAlpha](world)) assert.NilError(t, ecs.RegisterComponent[EnergyComponentBeta](world)) @@ -504,7 +505,7 @@ func (C) Name() string { return "c" } func (D) Name() string { return "d" } func TestQueriesAndFiltersWorks(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[A](world)) assert.NilError(t, ecs.RegisterComponent[B](world)) assert.NilError(t, ecs.RegisterComponent[C](world)) @@ -563,7 +564,7 @@ func (HealthComponent) Name() string { } func TestUpdateWithPointerType(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[HealthComponent](world)) assert.NilError(t, world.LoadGameState()) @@ -594,7 +595,7 @@ func (ValueComponent1) Name() string { } func TestCanRemoveFirstEntity(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[ValueComponent1](world)) wCtx := ecs.NewWorldContext(world) @@ -631,7 +632,7 @@ func (OtherComponent) Name() string { } func TestCanChangeArchetypeOfFirstEntity(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[ValueComponent2](world)) assert.NilError(t, ecs.RegisterComponent[OtherComponent](world)) @@ -654,7 +655,7 @@ func TestCanChangeArchetypeOfFirstEntity(t *testing.T) { } func TestEntityCreationAndSetting(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[ValueComponent2](world)) assert.NilError(t, ecs.RegisterComponent[OtherComponent](world)) diff --git a/cardinal/ecs/filter/filter_test.go b/cardinal/ecs/filter/filter_test.go index 0d9308209..aaaa511d5 100644 --- a/cardinal/ecs/filter/filter_test.go +++ b/cardinal/ecs/filter/filter_test.go @@ -2,6 +2,7 @@ package filter_test import ( "fmt" + "pkg.world.dev/world-engine/cardinal/testutils" "github.com/rs/zerolog" "testing" @@ -22,7 +23,7 @@ func (gammaComponent) Name() string { } func TestGetEverythingFilter(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[Alpha](world)) assert.NilError(t, ecs.RegisterComponent[Beta](world)) @@ -52,7 +53,7 @@ func TestGetEverythingFilter(t *testing.T) { } func TestCanFilterByArchetype(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[Alpha](world)) assert.NilError(t, ecs.RegisterComponent[Beta](world)) @@ -97,7 +98,7 @@ type Gamma struct{} func (Gamma) Name() string { return "gamma" } func TestExactVsContains(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[Alpha](world)) assert.NilError(t, ecs.RegisterComponent[Beta](world)) @@ -213,7 +214,7 @@ func TestExactVsContains(t *testing.T) { } func TestCanGetArchetypeFromEntity(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[Alpha](world)) assert.NilError(t, ecs.RegisterComponent[Beta](world)) assert.NilError(t, world.LoadGameState()) @@ -262,7 +263,7 @@ func TestCanGetArchetypeFromEntity(t *testing.T) { func BenchmarkEntityCreation(b *testing.B) { zerolog.SetGlobalLevel(zerolog.Disabled) for i := 0; i < b.N; i++ { - world := ecs.NewTestWorld(b) + world := testutils.NewTestWorld(b).Instance() assert.NilError(b, ecs.RegisterComponent[Alpha](world)) assert.NilError(b, world.LoadGameState()) wCtx := ecs.NewWorldContext(world) @@ -287,7 +288,7 @@ func BenchmarkFilterByArchetypeIsNotImpactedByTotalEntityCount(b *testing.B) { func helperArchetypeFilter(b *testing.B, relevantCount, ignoreCount int) { b.StopTimer() - world := ecs.NewTestWorld(b) + world := testutils.NewTestWorld(b).Instance() assert.NilError(b, ecs.RegisterComponent[Alpha](world)) assert.NilError(b, ecs.RegisterComponent[Beta](world)) assert.NilError(b, world.LoadGameState()) diff --git a/cardinal/ecs/log/log_test.go b/cardinal/ecs/log/log_test.go index e427dda10..a1c732cef 100644 --- a/cardinal/ecs/log/log_test.go +++ b/cardinal/ecs/log/log_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "pkg.world.dev/world-engine/cardinal/testutils" "strings" "testing" "time" @@ -77,7 +78,7 @@ func TestWarningLogIfDuplicateSystemRegistered(t *testing.T) { } func TestWorldLogger(t *testing.T) { - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() // replaces internal Logger with one that logs to the buf variable above. var buf bytes.Buffer bufLogger := zerolog.New(&buf) diff --git a/cardinal/ecs/message/message_test.go b/cardinal/ecs/message/message_test.go index 750a0238d..99fd1bf74 100644 --- a/cardinal/ecs/message/message_test.go +++ b/cardinal/ecs/message/message_test.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/stretchr/testify/require" "pkg.world.dev/world-engine/cardinal/ecs/message" + "pkg.world.dev/world-engine/cardinal/testutils" "testing" "time" @@ -50,7 +51,7 @@ func TestReadTypeNotStructs(t *testing.T) { } func TestCanQueueTransactions(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() // Create an entity with a score component assert.NilError(t, ecs.RegisterComponent[ScoreComponent](world)) @@ -113,7 +114,7 @@ func (CounterComponent) Name() string { } func TestSystemsAreExecutedDuringGameTick(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[CounterComponent](world)) @@ -138,7 +139,7 @@ func TestSystemsAreExecutedDuringGameTick(t *testing.T) { } func TestTransactionAreAppliedToSomeEntities(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[ScoreComponent](world)) modifyScoreMsg := ecs.NewMessageType[*ModifyScoreMsg, *EmptyMsgResult]("modify_score") @@ -196,7 +197,7 @@ func TestTransactionAreAppliedToSomeEntities(t *testing.T) { // TestAddToQueueDuringTickDoesNotTimeout verifies that we can add a transaction to the transaction // queue during a game tick, and the call does not block. func TestAddToQueueDuringTickDoesNotTimeout(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() modScore := ecs.NewMessageType[*ModifyScoreMsg, *EmptyMsgResult]("modify_Score") assert.NilError(t, world.RegisterMessages(modScore)) @@ -238,7 +239,7 @@ func TestAddToQueueDuringTickDoesNotTimeout(t *testing.T) { // TestTransactionsAreExecutedAtNextTick verifies that while a game tick is taking place, new transactions // are added to some queue that is not processed until the NEXT tick. func TestTransactionsAreExecutedAtNextTick(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() modScoreMsg := ecs.NewMessageType[*ModifyScoreMsg, *EmptyMsgResult]("modify_score") assert.NilError(t, world.RegisterMessages(modScoreMsg)) ctx := context.Background() @@ -309,7 +310,7 @@ func TestTransactionsAreExecutedAtNextTick(t *testing.T) { // TestIdenticallyTypedTransactionCanBeDistinguished verifies that two transactions of the same type // can be distinguished if they were added with different MessageType[T]s. func TestIdenticallyTypedTransactionCanBeDistinguished(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() type NewOwner struct { Name string } @@ -338,13 +339,13 @@ func TestIdenticallyTypedTransactionCanBeDistinguished(t *testing.T) { func TestCannotRegisterDuplicateTransaction(t *testing.T) { msg := ecs.NewMessageType[ModifyScoreMsg, EmptyMsgResult]("modify_score") - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.Check(t, nil != world.RegisterMessages(msg, msg)) } func TestCannotCallRegisterTransactionsMultipleTimes(t *testing.T) { msg := ecs.NewMessageType[ModifyScoreMsg, EmptyMsgResult]("modify_score") - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, world.RegisterMessages(msg)) assert.Check(t, nil != world.RegisterMessages(msg)) } @@ -386,7 +387,7 @@ func TestCannotHaveDuplicateTransactionNames(t *testing.T) { type OtherMsg struct { Alpha, Beta string } - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() alphaMsg := ecs.NewMessageType[SomeMsg, EmptyMsgResult]("name_match") betaMsg := ecs.NewMessageType[OtherMsg, EmptyMsgResult]("name_match") assert.ErrorIs(t, world.RegisterMessages(alphaMsg, betaMsg), ecs.ErrDuplicateMessageName) @@ -399,7 +400,7 @@ func TestCanGetTransactionErrorsAndResults(t *testing.T) { type MoveMsgResult struct { EndX, EndY int } - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() // Each transaction now needs an input and an output moveMsg := ecs.NewMessageType[MoveMsg, MoveMsgResult]("move") @@ -458,7 +459,7 @@ func TestSystemCanFindErrorsFromEarlierSystem(t *testing.T) { type MsgOut struct { Number int } - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() numTx := ecs.NewMessageType[MsgIn, MsgOut]("number") assert.NilError(t, world.RegisterMessages(numTx)) wantErr := errors.New("some transaction error") @@ -500,7 +501,7 @@ func TestSystemCanClobberTransactionResult(t *testing.T) { type MsgOut struct { Number int } - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() numTx := ecs.NewMessageType[MsgIn, MsgOut]("number") assert.NilError(t, world.RegisterMessages(numTx)) systemCalls := 0 diff --git a/cardinal/ecs/message_test.go b/cardinal/ecs/message_test.go index 4c58fc9d7..ef93e543d 100644 --- a/cardinal/ecs/message_test.go +++ b/cardinal/ecs/message_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "pkg.world.dev/world-engine/cardinal/ecs/message" + "pkg.world.dev/world-engine/cardinal/testutils" "testing" "gotest.tools/v3/assert" @@ -13,7 +14,7 @@ import ( ) func TestForEachTransaction(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() type SomeMsgRequest struct { GenerateError bool } diff --git a/cardinal/ecs/mock_world.go b/cardinal/ecs/mock_world.go deleted file mode 100644 index 5ffba2d2b..000000000 --- a/cardinal/ecs/mock_world.go +++ /dev/null @@ -1,63 +0,0 @@ -package ecs - -import ( - "fmt" - "testing" - - "gotest.tools/v3/assert" - - "github.com/alicebob/miniredis/v2" - "github.com/rs/zerolog/log" - "pkg.world.dev/world-engine/cardinal/ecs/ecb" - "pkg.world.dev/world-engine/cardinal/ecs/storage" -) - -// NewMockWorld creates an ecs.World that uses a mock redis DB as the storage -// layer. This is only suitable for local development. If you are creating an ecs.World for -// unit tests, use NewTestWorld. -func NewMockWorld(opts ...Option) (world *World, cleanup func()) { - // We manually set the start address to make the port deterministic - s := miniredis.NewMiniRedis() - err := s.StartAddr(":12345") - if err != nil { - panic("Unable to initialize in-memory redis") - } - log.Logger.Debug().Msgf("miniredis started at %s", s.Addr()) - - w, err := newMockWorld(s, opts...) - if err != nil { - panic(fmt.Errorf("unable to initialize world: %w", err)) - } - - return w, func() { - log.Logger.Debug().Msg("miniredis shutting down") - s.Close() - log.Logger.Debug().Msg("successfully shut down miniredis") - } -} - -// NewTestWorld creates an ecs.World suitable for running in tests. Relevant resources -// are automatically cleaned up at the completion of each test. -func NewTestWorld(t testing.TB, opts ...Option) *World { - s := miniredis.RunT(t) - w, err := newMockWorld(s, opts...) - if err != nil { - t.Fatalf("Unable to initialize world: %v", err) - } - assert.NilError(t, err) - return w -} - -func newMockWorld(s *miniredis.Miniredis, opts ...Option) (*World, error) { - redisStore := storage.NewRedisStorage(storage.Options{ - Addr: s.Addr(), - Password: "", // no password set - DB: 0, // use default DB - }, "in-memory-world") - entityStore, err := ecb.NewManager(redisStore.Client) - if err != nil { - return nil, err - } - - return NewWorld(&redisStore, entityStore, opts...) -} diff --git a/cardinal/ecs/persona_test.go b/cardinal/ecs/persona_test.go index 116f1300c..b3001099f 100644 --- a/cardinal/ecs/persona_test.go +++ b/cardinal/ecs/persona_test.go @@ -3,6 +3,7 @@ package ecs_test import ( "context" "fmt" + "pkg.world.dev/world-engine/cardinal/testutils" "testing" "pkg.world.dev/world-engine/cardinal/ecs/component" @@ -16,7 +17,7 @@ import ( func TestCreatePersonaTransactionAutomaticallyCreated(t *testing.T) { // Verify that the CreatePersona is automatically created and registered with a world. - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, world.LoadGameState()) wantTag := "CoolMage" @@ -52,7 +53,7 @@ func TestCreatePersonaTransactionAutomaticallyCreated(t *testing.T) { } func TestGetSignerForPersonaTagReturnsErrorWhenNotRegistered(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, world.LoadGameState()) ctx := context.Background() @@ -85,7 +86,7 @@ func TestGetSignerForPersonaTagReturnsErrorWhenNotRegistered(t *testing.T) { } func TestDuplicatePersonaTagsInTickAreOnlyRegisteredOnce(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, world.LoadGameState()) personaTag := "jeff" @@ -123,7 +124,7 @@ func TestDuplicatePersonaTagsInTickAreOnlyRegisteredOnce(t *testing.T) { func TestCanAuthorizeAddress(t *testing.T) { // Verify that the CreatePersona is automatically created and registered with a world. - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, world.LoadGameState()) wantTag := "CoolMage" diff --git a/cardinal/ecs/query_test.go b/cardinal/ecs/query_test.go index e25691f75..6ac3d79d2 100644 --- a/cardinal/ecs/query_test.go +++ b/cardinal/ecs/query_test.go @@ -3,6 +3,7 @@ package ecs_test import ( "context" "github.com/stretchr/testify/require" + "pkg.world.dev/world-engine/cardinal/testutils" "testing" "pkg.world.dev/world-engine/cardinal/evm" @@ -66,7 +67,7 @@ func TestQueryEVM(t *testing.T) { return expectedReply, nil }, ecs.WithQueryEVMSupport[FooRequest, FooReply]) - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() err := w.RegisterQueries(fooQuery) assert.NilError(t, err) err = w.RegisterMessages(ecs.NewMessageType[struct{}, struct{}]("blah")) diff --git a/cardinal/ecs/search_test.go b/cardinal/ecs/search_test.go index 1d5207bd2..c9b8a0a76 100644 --- a/cardinal/ecs/search_test.go +++ b/cardinal/ecs/search_test.go @@ -1,6 +1,7 @@ package ecs_test import ( + "pkg.world.dev/world-engine/cardinal/testutils" "testing" "gotest.tools/v3/assert" @@ -19,7 +20,7 @@ func (FooComponent) Name() string { } func TestSearchEarlyTermination(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[FooComponent](world)) total := 10 diff --git a/cardinal/ecs/tick_test.go b/cardinal/ecs/tick_test.go index 654b51538..d6b898e3b 100644 --- a/cardinal/ecs/tick_test.go +++ b/cardinal/ecs/tick_test.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "pkg.world.dev/world-engine/cardinal/testutils" "testing" "gotest.tools/v3/assert" @@ -38,7 +39,7 @@ func TestTickHappyPath(t *testing.T) { assert.Equal(t, uint64(10), twoWorld.CurrentTick()) } func TestIfPanicMessageLogged(t *testing.T) { - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() // replaces internal Logger with one that logs to the buf variable above. var buf bytes.Buffer bufLogger := zerolog.New(&buf) @@ -186,7 +187,7 @@ func (ScalarComponentBeta) Name() string { } func TestCanModifyArchetypeAndGetEntity(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[ScalarComponentAlpha](world)) assert.NilError(t, ecs.RegisterComponent[ScalarComponentBeta](world)) assert.NilError(t, world.LoadGameState()) diff --git a/cardinal/ecs/world_test.go b/cardinal/ecs/world_test.go index 35d69edf6..7f02cc5fa 100644 --- a/cardinal/ecs/world_test.go +++ b/cardinal/ecs/world_test.go @@ -3,6 +3,7 @@ package ecs_test import ( "context" "errors" + "pkg.world.dev/world-engine/cardinal/testutils" "testing" "time" @@ -15,7 +16,7 @@ import ( ) func TestCanWaitForNextTick(t *testing.T) { - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() startTickCh := make(chan time.Time) doneTickCh := make(chan uint64) assert.NilError(t, w.LoadGameState()) @@ -46,7 +47,7 @@ func TestCanWaitForNextTick(t *testing.T) { } func TestWaitForNextTickReturnsFalseWhenWorldIsShutDown(t *testing.T) { - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() startTickCh := make(chan time.Time) doneTickCh := make(chan uint64) assert.NilError(t, w.LoadGameState()) @@ -87,7 +88,7 @@ func TestWaitForNextTickReturnsFalseWhenWorldIsShutDown(t *testing.T) { } func TestCannotWaitForNextTickAfterWorldIsShutDown(t *testing.T) { - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() startTickCh := make(chan time.Time) doneTickCh := make(chan uint64) assert.NilError(t, w.LoadGameState()) @@ -113,7 +114,7 @@ func TestEVMTxConsume(t *testing.T) { type FooOut struct { Y string } - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() fooTx := ecs.NewMessageType[FooIn, FooOut]("foo", ecs.WithMsgEVMSupport[FooIn, FooOut]) assert.NilError(t, w.RegisterMessages(fooTx)) var returnVal FooOut @@ -166,8 +167,8 @@ func TestAddSystems(t *testing.T) { return nil } - w := ecs.NewTestWorld(t) - w.RegisterSystems(sys, sys, sys) + w := testutils.NewTestWorld(t).Instance() + w.RegiterSystems(sys, sys, sys) err := w.LoadGameState() assert.NilError(t, err) @@ -178,7 +179,7 @@ func TestAddSystems(t *testing.T) { } func TestSystemExecutionOrder(t *testing.T) { - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() order := make([]int, 0, 3) w.RegisterSystems(func(ecs.WorldContext) error { order = append(order, 1) @@ -200,13 +201,14 @@ func TestSystemExecutionOrder(t *testing.T) { } func TestSetNamespace(t *testing.T) { - id := "foo" - w := ecs.NewTestWorld(t, ecs.WithNamespace(id)) - assert.Equal(t, w.Namespace().String(), id) + namespace := "test" + t.Setenv("CARDINAL_NAMESPACE", namespace) + w := testutils.NewTestWorld(t).Instance() + assert.Equal(t, w.Namespace().String(), namespace) } func TestWithoutRegistration(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() wCtx := ecs.NewWorldContext(world) id, err := component.Create(wCtx, EnergyComponent{}, OwnableComponent{}) assert.Assert(t, err != nil) diff --git a/cardinal/events/events_test.go b/cardinal/events/events_test.go index 5e5f75f39..eaa087e93 100644 --- a/cardinal/events/events_test.go +++ b/cardinal/events/events_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "pkg.world.dev/world-engine/cardinal" "pkg.world.dev/world-engine/cardinal/testutils" "strings" "sync" @@ -24,7 +25,7 @@ import ( func TestEvents(t *testing.T) { // broadcast 5 messages to 5 clients means 25 messages received. numberToTest := 5 - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() assert.NilError(t, w.LoadGameState()) txh := testutils.MakeTestTransactionHandler(t, w, server.DisableSignatureVerification()) url := txh.MakeWebSocketURL("events") @@ -89,7 +90,7 @@ type SendEnergyTxResult struct{} func TestEventsThroughSystems(t *testing.T) { numberToTest := 5 - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() sendTx := ecs.NewMessageType[SendEnergyTx, SendEnergyTxResult]("send-energy") assert.NilError(t, w.RegisterMessages(sendTx)) counter1 := atomic.Int32{} @@ -154,7 +155,7 @@ func TestEventHubLogger(t *testing.T) { cardinalLogger := ecslog.Logger{ &bufLogger, } - w := ecs.NewTestWorld(t, ecs.WithLoggingEventHub(&cardinalLogger)) + w := testutils.NewTestWorld(t, cardinal.WithLoggingEventHub(&cardinalLogger)).Instance() numberToTest := 5 for i := 0; i < numberToTest; i++ { w.RegisterSystem(func(wCtx ecs.WorldContext) error { diff --git a/cardinal/evm/server_test.go b/cardinal/evm/server_test.go index ad325848c..3f5cff2cd 100644 --- a/cardinal/evm/server_test.go +++ b/cardinal/evm/server_test.go @@ -1,7 +1,9 @@ -package evm +package evm_test import ( "context" + "pkg.world.dev/world-engine/cardinal/evm" + "pkg.world.dev/world-engine/cardinal/testutils" "strings" "testing" "time" @@ -29,7 +31,7 @@ type TxReply struct{} // the world, and executed in systems. func TestServer_SendMessage(t *testing.T) { // setup the world - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() // create the ECS transactions fooTx := ecs.NewMessageType[FooTransaction, TxReply]("footx", ecs.WithMsgEVMSupport[FooTransaction, TxReply]) @@ -93,7 +95,7 @@ func TestServer_SendMessage(t *testing.T) { tickStartCh <- time.Now() <-tickDoneCh - server, err := NewServer(w) + server, err := evm.NewServer(w) assert.NilError(t, err) txSequenceDone := make(chan struct{}) @@ -150,12 +152,12 @@ func TestServer_Query(t *testing.T) { query := ecs.NewQueryType[FooReq, FooReply]("foo", func(wCtx ecs.WorldContext, req FooReq) (FooReply, error) { return FooReply{Y: req.X}, nil }, ecs.WithQueryEVMSupport[FooReq, FooReply]) - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() err := w.RegisterQueries(query) assert.NilError(t, err) err = w.RegisterMessages(ecs.NewMessageType[struct{}, struct{}]("nothing")) assert.NilError(t, err) - s, err := NewServer(w) + s, err := evm.NewServer(w) assert.NilError(t, err) request := FooReq{X: 3000} @@ -180,7 +182,7 @@ func TestServer_Query(t *testing.T) { // Authorized address for the sender, an error occurs. func TestServer_UnauthorizedAddress(t *testing.T) { // setup the world - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() // create the ECS transactions fooTxType := ecs.NewMessageType[FooTransaction, TxReply]("footx", ecs.WithMsgEVMSupport[FooTransaction, TxReply]) @@ -193,7 +195,7 @@ func TestServer_UnauthorizedAddress(t *testing.T) { assert.NilError(t, w.LoadGameState()) - server, err := NewServer(w) + server, err := evm.NewServer(w) assert.NilError(t, err) fooTxBz, err := fooTxType.ABIEncode(fooTx) @@ -206,6 +208,6 @@ func TestServer_UnauthorizedAddress(t *testing.T) { Message: fooTxBz, MessageId: fooTxType.Name(), }) - assert.Equal(t, res.Code, uint32(CodeUnauthorized)) + assert.Equal(t, res.Code, uint32(evm.CodeUnauthorized)) assert.Check(t, strings.Contains(res.Errs, "failed to authorize")) } diff --git a/cardinal/message.go b/cardinal/message.go index 5f402aad5..5bd8160cf 100644 --- a/cardinal/message.go +++ b/cardinal/message.go @@ -40,7 +40,7 @@ func NewMessageTypeWithEVMSupport[Input, Result any](name string) *MessageType[I // AddToQueue is not meant to be used in production whatsoever, it is exposed here for usage in tests. func (t *MessageType[Input, Result]) AddToQueue(world *World, data Input, sigs ...*sign.Transaction) TxHash { - txHash := t.impl.AddToQueue(world.implWorld, data, sigs...) + txHash := t.impl.AddToQueue(world.instance, data, sigs...) return txHash } diff --git a/cardinal/option.go b/cardinal/option.go index 8c8d295fb..4bb4ee821 100644 --- a/cardinal/option.go +++ b/cardinal/option.go @@ -1,6 +1,11 @@ package cardinal import ( + "github.com/alicebob/miniredis/v2" + "github.com/rs/zerolog/log" + ecslog "pkg.world.dev/world-engine/cardinal/ecs/log" + "pkg.world.dev/world-engine/cardinal/ecs/store" + "pkg.world.dev/world-engine/cardinal/events" "time" "pkg.world.dev/world-engine/cardinal/ecs" @@ -88,3 +93,41 @@ func WithCORS() WorldOption { serverOption: server.WithCORS(), } } + +func WithStoreManager(s store.IManager) WorldOption { + return WorldOption{ + ecsOption: ecs.WithStoreManager(s), + } +} + +func WithEventHub(eventHub events.EventHub) WorldOption { + return WorldOption{ + ecsOption: ecs.WithEventHub(eventHub), + } +} + +func WithLoggingEventHub(logger *ecslog.Logger) WorldOption { + return WorldOption{ + ecsOption: ecs.WithLoggingEventHub(logger), + } +} + +func withMockRedis() WorldOption { + // We manually set the start address to make the port deterministic + s := miniredis.NewMiniRedis() + err := s.StartAddr(":6379") + if err != nil { + panic("Unable to start miniredis. Make sure there is no other redis instance running on port 6379") + } + log.Logger.Debug().Msgf("miniredis started at %s", s.Addr()) + + return WorldOption{ + cardinalOption: func(world *World) { + world.cleanup = func() { + log.Logger.Debug().Msg("miniredis shutting down") + s.Close() + log.Logger.Debug().Msg("miniredis shutdown successful") + } + }, + } +} diff --git a/cardinal/server/server_test.go b/cardinal/server/server_test.go index d671a4ff1..04e556d8b 100644 --- a/cardinal/server/server_test.go +++ b/cardinal/server/server_test.go @@ -10,6 +10,7 @@ import ( "net/http" "os" "os/exec" + "pkg.world.dev/world-engine/cardinal" "reflect" "strconv" "testing" @@ -41,7 +42,7 @@ type SendEnergyTxResult struct{} func TestHealthEndpoint(t *testing.T) { testutils.SetTestTimeout(t, 10*time.Second) - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() assert.NilError(t, w.LoadGameState()) testutils.MakeTestTransactionHandler(t, w, server.DisableSignatureVerification()) resp, err := http.Get("http://localhost:4040/health") @@ -80,7 +81,7 @@ type Gamma struct{} func (Gamma) Name() string { return "gamma" } func TestDebugEndpoint(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, ecs.RegisterComponent[Alpha](world)) assert.NilError(t, ecs.RegisterComponent[Beta](world)) @@ -119,7 +120,7 @@ func TestDebugEndpoint(t *testing.T) { func TestShutDownViaMethod(t *testing.T) { // If this test is frozen then it failed to shut down, create failure with panic. testutils.SetTestTimeout(t, 10*time.Second) - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() assert.NilError(t, w.LoadGameState()) txh := testutils.MakeTestTransactionHandler(t, w, server.DisableSignatureVerification()) resp, err := http.Get("http://localhost:4040/health") @@ -142,7 +143,7 @@ func TestShutDownViaMethod(t *testing.T) { func TestShutDownViaSignal(t *testing.T) { // If this test is frozen then it failed to shut down, create a failure with panic. testutils.SetTestTimeout(t, 10*time.Second) - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() sendTx := ecs.NewMessageType[SendEnergyTx, SendEnergyTxResult]("sendTx") assert.NilError(t, w.RegisterMessages(sendTx)) w.RegisterSystem(func(ecs.WorldContext) error { @@ -175,7 +176,7 @@ func TestShutDownViaSignal(t *testing.T) { } func TestIfServeSetEnvVarForPort(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() alphaTx := ecs.NewMessageType[SendEnergyTx, SendEnergyTxResult]("alpha") assert.NilError(t, world.RegisterMessages(alphaTx)) txh, err := server.NewHandler(world, nil, server.DisableSignatureVerification()) @@ -198,7 +199,7 @@ func TestIfServeSetEnvVarForPort(t *testing.T) { } func TestCanListTransactionEndpoints(t *testing.T) { - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() alphaTx := ecs.NewMessageType[SendEnergyTx, SendEnergyTxResult]("alpha") betaTx := ecs.NewMessageType[SendEnergyTx, SendEnergyTxResult]("beta") gammaTx := ecs.NewMessageType[SendEnergyTx, SendEnergyTxResult]("gamma") @@ -243,7 +244,7 @@ func mustReadBody(t *testing.T, resp *http.Response) string { func TestHandleTransactionWithNoSignatureVerification(t *testing.T) { endpoint := "move" url := "tx/game/" + endpoint - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() sendTx := ecs.NewMessageType[SendEnergyTx, SendEnergyTxResult](endpoint) assert.NilError(t, w.RegisterMessages(sendTx)) count := 0 @@ -301,7 +302,7 @@ type garbageStructBeta struct { func (garbageStructBeta) Name() string { return "beta" } func TestHandleSwaggerServer(t *testing.T) { - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() sendTx := ecs.NewMessageType[SendEnergyTx, SendEnergyTxResult]("send-energy") assert.NilError(t, w.RegisterMessages(sendTx)) w.RegisterSystem(func(ecs.WorldContext) error { @@ -494,7 +495,7 @@ func TestHandleWrappedTransactionWithNoSignatureVerification(t *testing.T) { endpoint := "move" url := fmt.Sprintf("tx/game/%s", endpoint) count := 0 - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() sendTx := ecs.NewMessageType[SendEnergyTx, SendEnergyTxResult](endpoint) assert.NilError(t, w.RegisterMessages(sendTx)) w.RegisterSystem(func(wCtx ecs.WorldContext) error { @@ -538,7 +539,7 @@ func TestHandleWrappedTransactionWithNoSignatureVerification(t *testing.T) { func TestCanCreateAndVerifyPersonaSigner(t *testing.T) { urlSet := []string{"tx/persona/create-persona", "query/persona/signer"} - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() tx := ecs.NewMessageType[SendEnergyTx, SendEnergyTxResult]("some_tx") assert.NilError(t, world.RegisterMessages(tx)) assert.NilError(t, world.LoadGameState()) @@ -608,7 +609,8 @@ func TestCanCreateAndVerifyPersonaSigner(t *testing.T) { func TestSigVerificationChecksNamespace(t *testing.T) { url := "tx/persona/create-persona" - world := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t) + world := w.Instance() assert.NilError(t, world.LoadGameState()) privateKey, err := crypto.GenerateKey() assert.NilError(t, err) @@ -645,7 +647,7 @@ func TestSigVerificationChecksNamespace(t *testing.T) { func TestSigVerificationChecksNonce(t *testing.T) { url := "tx/persona/create-persona" - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, world.LoadGameState()) privateKey, err := crypto.GenerateKey() assert.NilError(t, err) @@ -698,7 +700,7 @@ func TestSigVerificationChecksNonce(t *testing.T) { // TestCanListQueries tests that we can list the available queries in the handler. func TestCanListQueries(t *testing.T) { - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() type FooRequest struct { Foo int `json:"foo,omitempty"` Meow string `json:"bar,omitempty"` @@ -769,7 +771,7 @@ func TestQueryEncodeDecode(t *testing.T) { url := "query/game/" + endpoint // set up the world, register the queries, load. - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, world.RegisterQueries(fq)) assert.NilError(t, world.LoadGameState()) @@ -798,7 +800,7 @@ func TestQueryEncodeDecode(t *testing.T) { func TestMalformedRequestToGetTransactionReceiptsProducesError(t *testing.T) { url := "query/receipts/list" - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() assert.NilError(t, world.LoadGameState()) txh := testutils.MakeTestTransactionHandler(t, world, server.DisableSignatureVerification()) res := txh.Post(url, map[string]any{ @@ -813,7 +815,8 @@ func TestTransactionReceiptReturnCorrectTickWindows(t *testing.T) { url := "query/receipts/list" historySize := uint64(10) - world := ecs.NewTestWorld(t, ecs.WithReceiptHistorySize(int(historySize))) + world := testutils.NewTestWorld(t, cardinal.WithReceiptHistorySize(int(historySize))).Instance() + assert.NilError(t, world.LoadGameState()) txh := testutils.MakeTestTransactionHandler(t, world, server.DisableSignatureVerification()) @@ -905,7 +908,8 @@ func TestCanGetTransactionReceiptsSwagger(t *testing.T) { dupeTx := ecs.NewMessageType[DupeRequest, DupeReply]("duplicate") errTx := ecs.NewMessageType[ErrRequest, ErrReply]("error") - world := ecs.NewTestWorld(t) + world := testutils.NewTestWorld(t).Instance() + assert.NilError(t, world.RegisterMessages(incTx, dupeTx, errTx)) // System to handle incrementing numbers world.RegisterSystem(func(wCtx ecs.WorldContext) error { @@ -1028,7 +1032,10 @@ func TestTransactionIDIsReturned(t *testing.T) { swaggerUrls := []string{swaggerCreatePersonURL, "tx/game/move"} urls := swaggerUrls type MoveTx struct{} - world := ecs.NewTestWorld(t) + + w := testutils.NewTestWorld(t) + world := w.Instance() + moveTx := ecs.NewMessageType[MoveTx, MoveTx]("move") assert.NilError(t, world.RegisterMessages(moveTx)) assert.NilError(t, world.LoadGameState()) @@ -1114,7 +1121,9 @@ func TestTransactionsSubmittedToChain(t *testing.T) { type MoveTx struct { Direction string } - world := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t) + world := w.Instance() + moveTx := ecs.NewMessageType[MoveTx, MoveTx]("move") assert.NilError(t, world.RegisterMessages(moveTx)) assert.NilError(t, world.LoadGameState()) @@ -1158,7 +1167,9 @@ func TestTransactionNotSubmittedWhenRecovering(t *testing.T) { } holdChan := make(chan bool) adapter := adapterMock{hold: holdChan} - world := ecs.NewTestWorld(t, ecs.WithAdapter(&adapter)) + w := testutils.NewTestWorld(t, cardinal.WithAdapter(&adapter)) + world := w.Instance() + go func() { err := world.RecoverFromChain(context.Background()) assert.NilError(t, err) @@ -1188,9 +1199,10 @@ func TestTransactionNotSubmittedWhenRecovering(t *testing.T) { } func TestWebSocket(t *testing.T) { - w := ecs.NewTestWorld(t) - assert.NilError(t, w.LoadGameState()) - txh := testutils.MakeTestTransactionHandler(t, w, server.DisableSignatureVerification()) + w := testutils.NewTestWorld(t) + world := w.Instance() + assert.NilError(t, w.Instance().LoadGameState()) + txh := testutils.MakeTestTransactionHandler(t, world, server.DisableSignatureVerification()) url := txh.MakeWebSocketURL("echo") dial, _, err := websocket.DefaultDialer.Dial(url, nil) assert.NilError(t, err) diff --git a/cardinal/testing.go b/cardinal/testing.go index 4896261fa..0033c8c44 100644 --- a/cardinal/testing.go +++ b/cardinal/testing.go @@ -5,7 +5,7 @@ import "pkg.world.dev/world-engine/cardinal/ecs" // This file contains helper methods that should only be used in the context of running tests. func TestingWorldToWorldContext(world *World) WorldContext { - ecsWorldCtx := ecs.NewWorldContext(world.implWorld) + ecsWorldCtx := ecs.NewWorldContext(world.instance) return &worldContext{implContext: ecsWorldCtx} } @@ -14,7 +14,7 @@ func TestingWorldContextToECSWorld(worldCtx WorldContext) *ecs.World { } func (w *World) TestingGetTransactionReceiptsForTick(tick uint64) ([]Receipt, error) { - return w.implWorld.GetTransactionReceiptsForTick(tick) + return w.instance.GetTransactionReceiptsForTick(tick) } // The following type and function are exported temporarily pending a refactor of @@ -22,5 +22,5 @@ func (w *World) TestingGetTransactionReceiptsForTick(tick uint64) ([]Receipt, er type CreatePersonaTransaction = ecs.CreatePersona func (w *World) TestingAddCreatePersonaTxToQueue(data CreatePersonaTransaction) { - ecs.CreatePersonaMsg.AddToQueue(w.implWorld, data) + ecs.CreatePersonaMsg.AddToQueue(w.instance, data) } diff --git a/cardinal/testutils/test_utils.go b/cardinal/testutils/test_utils.go index 8d781f692..330c896d8 100644 --- a/cardinal/testutils/test_utils.go +++ b/cardinal/testutils/test_utils.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/http" + "pkg.world.dev/world-engine/cardinal/ecs" "sync" "testing" "time" @@ -16,7 +17,6 @@ import ( "github.com/ethereum/go-ethereum/crypto" "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/ecs" "pkg.world.dev/world-engine/cardinal/events" "pkg.world.dev/world-engine/cardinal/server" "pkg.world.dev/world-engine/sign" @@ -189,20 +189,15 @@ func AddTransactionToWorldByAnyTransaction( // MakeWorldAndTicker sets up a cardinal.World as well as a function that can execute one game tick. The *cardinal.World // will be automatically started when doTick is called for the first time. The cardinal.World will be shut down at the // end of the test. If doTick takes longer than 5 seconds to run, t.Fatal will be called. -func MakeWorldAndTicker(t *testing.T, - opts ...cardinal.WorldOption) ( - world *cardinal.World, - doTick func()) { +func MakeWorldAndTicker(t *testing.T, opts ...cardinal.WorldOption) (world *cardinal.World, doTick func()) { startTickCh, doneTickCh := make(chan time.Time), make(chan uint64) - opts = append(opts, cardinal.WithTickChannel(startTickCh), cardinal.WithTickDoneChannel(doneTickCh)) - world, err := cardinal.NewMockWorld(opts...) - if err != nil { - t.Fatalf("unable to make mock world: %v", err) - } + eventHub := events.CreateWebSocketEventHub() + opts = append(opts, cardinal.WithTickChannel(startTickCh), cardinal.WithTickDoneChannel(doneTickCh), cardinal.WithEventHub(eventHub)) + world = NewTestWorld(t, opts...) // Shutdown any world resources. This will be called whether the world has been started or not. t.Cleanup(func() { - if err = world.ShutDown(); err != nil { + if err := world.ShutDown(); err != nil { t.Fatalf("unable to shut down world: %v", err) } }) @@ -222,7 +217,7 @@ func MakeWorldAndTicker(t *testing.T, }() for !world.IsGameRunning() { select { - case err = <-startupError: + case err := <-startupError: t.Fatalf("startup error: %v", err) case <-timeout: t.Fatal("timeout while waiting for game to start") diff --git a/cardinal/testutils/world.go b/cardinal/testutils/world.go new file mode 100644 index 000000000..1936dfd40 --- /dev/null +++ b/cardinal/testutils/world.go @@ -0,0 +1,24 @@ +package testutils + +import ( + "github.com/alicebob/miniredis/v2" + "gotest.tools/v3/assert" + "pkg.world.dev/world-engine/cardinal" + "testing" +) + +// NewTestWorld creates a World object suitable for unit tests. +// Relevant resources are automatically cleaned up at the completion of each test. +func NewTestWorld(t testing.TB, opts ...cardinal.WorldOption) *cardinal.World { + // Init testing environment + s := miniredis.RunT(t) + t.Setenv("CARDINAL_DEPLOY_MODE", "development") + t.Setenv("REDIS_ADDRESS", s.Addr()) + + world, err := cardinal.NewWorld(opts...) + if err != nil { + t.Fatalf("Unable to initialize test world: %v", err) + } + assert.NilError(t, err) + return world +} diff --git a/cardinal/world.go b/cardinal/world.go index 9c23b2f6d..044c035ed 100644 --- a/cardinal/world.go +++ b/cardinal/world.go @@ -28,7 +28,7 @@ import ( ) type World struct { - implWorld *ecs.World + instance *ecs.World server *server.Handler evmServer evm.Server gameManager *server.GameManager @@ -54,22 +54,27 @@ type ( ) // NewWorld creates a new World object using Redis as the storage layer. -func NewWorld(addr, password string, opts ...WorldOption) (*World, error) { +func NewWorld(opts ...WorldOption) (*World, error) { ecsOptions, serverOptions, cardinalOptions := separateOptions(opts) - log.Info().Msg("Running in normal mode, using external Redis") - if addr == "" { - return nil, errors.New("redis address is required") - } - if password == "" { - log.Info().Msg("Redis password is not set, make sure to set up redis with password in prod") - } + cfg := GetWorldConfig() + + if cfg.CardinalDeployMode == "production" { + log.Logger.Info().Msg("Starting a new Cardinal world in production mode") + if cfg.RedisPassword == "" { + return nil, errors.New("redis password is required in production") + } + } else { + if cfg.CardinalDeployMode != "development" { + log.Logger.Warn().Msg("CARDINAL_DEPLOY_MODE is unrecognized. Defaulting to development mode") + } + log.Logger.Info().Msg("Starting a new Cardinal world in development mode") + } redisStore := storage.NewRedisStorage(storage.Options{ - Addr: addr, - Password: password, // make sure to set this in prod - DB: 0, // use default DB - }, "world") - log.Info().Msgf("redis address: %s", addr) + Addr: cfg.RedisAddress, + Password: cfg.RedisPassword, + DB: 0, // use default DB + }, cfg.CardinalWorldId) storeManager, err := ecb.NewManager(redisStore.Client) if err != nil { return nil, err @@ -81,7 +86,7 @@ func NewWorld(addr, password string, opts ...WorldOption) (*World, error) { } world := &World{ - implWorld: ecsWorld, + instance: ecsWorld, serverOptions: serverOptions, endStartGame: make(chan bool), } @@ -93,22 +98,12 @@ func NewWorld(addr, password string, opts ...WorldOption) (*World, error) { return world, nil } -// NewMockWorld creates a World that uses an in-memory redis DB as the storage layer. -// This is only suitable for local development. +// NewMockWorld creates a World object that uses miniredis as the storage layer suitable for local development. +// If you are creating an ecs.World for unit tests, use NewTestWorld. func NewMockWorld(opts ...WorldOption) (*World, error) { - ecsOptions, serverOptions, cardinalOptions := separateOptions(opts) - eventHub := events.CreateWebSocketEventHub() - ecsOptions = append(ecsOptions, ecs.WithEventHub(eventHub)) - implWorld, mockWorldCleanup := ecs.NewMockWorld(ecsOptions...) - world := &World{ - implWorld: implWorld, - serverOptions: serverOptions, - cleanup: mockWorldCleanup, - endStartGame: make(chan bool), - } - world.isGameRunning.Store(false) - for _, opt := range cardinalOptions { - opt(world) + world, err := NewWorld(append(opts, withMockRedis())...) + if err != nil { + return world, err } return world, nil } @@ -181,26 +176,26 @@ func (w *World) StartGame() error { return errors.New("game already running") } - if err := w.implWorld.LoadGameState(); err != nil { + if err := w.instance.LoadGameState(); err != nil { return err } eventHub := events.CreateWebSocketEventHub() - w.implWorld.SetEventHub(eventHub) + w.instance.SetEventHub(eventHub) eventBuilder := events.CreateNewWebSocketBuilder("/events", events.CreateWebSocketEventHandler(eventHub)) - handler, err := server.NewHandler(w.implWorld, eventBuilder, w.serverOptions...) + handler, err := server.NewHandler(w.instance, eventBuilder, w.serverOptions...) if err != nil { return err } w.server = handler - w.evmServer, err = evm.NewServer(w.implWorld) + w.evmServer, err = evm.NewServer(w.instance) if err != nil { if !errors.Is(err, evm.ErrNoEVMTypes) { return err } - w.implWorld.Logger.Debug().Msg("no EVM messages or queries specified. EVM server will not run") + w.instance.Logger.Debug().Msg("no EVM messages or queries specified. EVM server will not run") } else { - w.implWorld.Logger.Debug().Msg("running world with EVM server") + w.instance.Logger.Debug().Msg("running world with EVM server") err = w.evmServer.Serve() if err != nil { return err @@ -210,8 +205,8 @@ func (w *World) StartGame() error { if w.tickChannel == nil { w.tickChannel = time.Tick(time.Second) //nolint:staticcheck // its ok. } - w.implWorld.StartGameLoop(context.Background(), w.tickChannel, w.tickDoneChannel) - gameManager := server.NewGameManager(w.implWorld, w.server) + w.instance.StartGameLoop(context.Background(), w.tickChannel, w.tickDoneChannel) + gameManager := server.NewGameManager(w.instance, w.server) w.gameManager = &gameManager go func() { w.isGameRunning.Store(true) @@ -254,7 +249,7 @@ func RegisterSystems(w *World, systems ...System) error { for _, system := range systems { functionName := filepath.Base(runtime.FuncForPC(reflect.ValueOf(system).Pointer()).Name()) sys := system - w.implWorld.RegisterSystemWithName(func(wCtx ecs.WorldContext) error { + w.instance.RegisterSystemWithName(func(wCtx ecs.WorldContext) error { return sys(&worldContext{ implContext: wCtx, }) @@ -264,32 +259,36 @@ func RegisterSystems(w *World, systems ...System) error { } func RegisterComponent[T metadata.Component](world *World) error { - return ecs.RegisterComponent[T](world.implWorld) + return ecs.RegisterComponent[T](world.instance) } // RegisterMessages adds the given messages to the game world. HTTP endpoints to queue up/execute these // messages will automatically be created when StartGame is called. This Register method must only be called once. func RegisterMessages(w *World, msgs ...AnyMessage) error { - return w.implWorld.RegisterMessages(toMessageType(msgs)...) + return w.instance.RegisterMessages(toMessageType(msgs)...) } // RegisterQueries adds the given query capabilities to the game world. HTTP endpoints to use these queries // will automatically be created when StartGame is called. This Register method must only be called once. func RegisterQueries(w *World, queries ...AnyQueryType) error { - return w.implWorld.RegisterQueries(toIQueryType(queries)...) + return w.instance.RegisterQueries(toIQueryType(queries)...) +} + +func (w *World) Instance() *ecs.World { + return w.instance } func (w *World) CurrentTick() uint64 { - return w.implWorld.CurrentTick() + return w.instance.CurrentTick() } func (w *World) Tick(ctx context.Context) error { - return w.implWorld.Tick(ctx) + return w.instance.Tick(ctx) } // Init Registers a system that only runs once on a new game before tick 0. func (w *World) Init(system System) { - w.implWorld.AddInitSystem(func(ecsWctx ecs.WorldContext) error { + w.instance.AddInitSystem(func(ecsWctx ecs.WorldContext) error { return system(&worldContext{implContext: ecsWctx}) }) } From ebfe7d4a008658f1c60d61a8ba42a9ce48090d8f Mon Sep 17 00:00:00 2001 From: smsunarto Date: Wed, 8 Nov 2023 19:44:45 -0800 Subject: [PATCH 02/14] delete cardinal/.gitignore --- cardinal/.gitignore | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 cardinal/.gitignore diff --git a/cardinal/.gitignore b/cardinal/.gitignore deleted file mode 100644 index 3cde4c9c8..000000000 --- a/cardinal/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -.idea/* From e3e664b0d004ff313e3932c1c305ea228ea2e47e Mon Sep 17 00:00:00 2001 From: smsunarto Date: Wed, 8 Nov 2023 19:52:44 -0800 Subject: [PATCH 03/14] remove outdate comment --- cardinal/cardinal_test.go | 1 - internal/e2e/tester/cardinal/main.go | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/cardinal/cardinal_test.go b/cardinal/cardinal_test.go index 31fee972e..5b30da5da 100644 --- a/cardinal/cardinal_test.go +++ b/cardinal/cardinal_test.go @@ -21,7 +21,6 @@ type Foo struct{} func (Foo) Name() string { return "foo" } func TestNewWorld(t *testing.T) { - // should fail, this test should generate a compile error if the function signature changes. _, err := cardinal.NewWorld(cardinal.WithNamespace("testnamespace")) assert.NilError(t, err) } diff --git a/internal/e2e/tester/cardinal/main.go b/internal/e2e/tester/cardinal/main.go index d03052446..c90bce169 100644 --- a/internal/e2e/tester/cardinal/main.go +++ b/internal/e2e/tester/cardinal/main.go @@ -14,7 +14,6 @@ import ( ) func main() { - redisAddr := os.Getenv("REDIS_ADDR") namespace := os.Getenv("NAMESPACE") options := []cardinal.WorldOption{ cardinal.WithNamespace(namespace), @@ -26,7 +25,7 @@ func main() { options = append(options, cardinal.WithAdapter(setupAdapter())) } - world, err := cardinal.NewWorld(redisAddr, "", options...) + world, err := cardinal.NewWorld(options...) if err != nil { log.Fatal(err) } From 7d0e1afb9bb91d9e51fbf8f345f9b041c21d0dd6 Mon Sep 17 00:00:00 2001 From: Scott Sunarto Date: Thu, 9 Nov 2023 12:58:39 -0800 Subject: [PATCH 04/14] Update cardinal/world.go Co-authored-by: Tyler <48813565+technicallyty@users.noreply.github.com> --- cardinal/world.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cardinal/world.go b/cardinal/world.go index 044c035ed..b21aa8379 100644 --- a/cardinal/world.go +++ b/cardinal/world.go @@ -101,7 +101,7 @@ func NewWorld(opts ...WorldOption) (*World, error) { // NewMockWorld creates a World object that uses miniredis as the storage layer suitable for local development. // If you are creating an ecs.World for unit tests, use NewTestWorld. func NewMockWorld(opts ...WorldOption) (*World, error) { - world, err := NewWorld(append(opts, withMockRedis())...) + world, err := NewWorld(withMockRedis(), opts...) if err != nil { return world, err } From dfb8a6e0331981e482d93b6459bbfe9010b2c28a Mon Sep 17 00:00:00 2001 From: Scott Sunarto Date: Thu, 9 Nov 2023 12:58:52 -0800 Subject: [PATCH 05/14] Update cardinal/world.go Co-authored-by: Tyler <48813565+technicallyty@users.noreply.github.com> --- cardinal/world.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cardinal/world.go b/cardinal/world.go index b21aa8379..ff0187abe 100644 --- a/cardinal/world.go +++ b/cardinal/world.go @@ -99,7 +99,7 @@ func NewWorld(opts ...WorldOption) (*World, error) { } // NewMockWorld creates a World object that uses miniredis as the storage layer suitable for local development. -// If you are creating an ecs.World for unit tests, use NewTestWorld. +// If you are creating a World for unit tests, use NewTestWorld. func NewMockWorld(opts ...WorldOption) (*World, error) { world, err := NewWorld(withMockRedis(), opts...) if err != nil { From af0da9a368de24e02ab8c76eaac014ccd44c37b4 Mon Sep 17 00:00:00 2001 From: smsunarto Date: Fri, 10 Nov 2023 15:57:49 -0800 Subject: [PATCH 06/14] address comments --- cardinal/cardinal_test.go | 4 +- cardinal/config.go | 33 ++++++++++++---- cardinal/ecs/benchmark/benchmark_test.go | 2 +- cardinal/ecs/internal/testutil/testutil.go | 16 +++++--- cardinal/ecs/storage/redis_storage.go | 16 ++++---- cardinal/ecs/world.go | 46 ++++++++++++++++------ cardinal/option.go | 19 +++------ cardinal/option_test.go | 1 - cardinal/world.go | 38 +++++++++++++----- 9 files changed, 117 insertions(+), 58 deletions(-) diff --git a/cardinal/cardinal_test.go b/cardinal/cardinal_test.go index 5b30da5da..e35330588 100644 --- a/cardinal/cardinal_test.go +++ b/cardinal/cardinal_test.go @@ -28,7 +28,7 @@ func TestNewWorld(t *testing.T) { func TestCanQueryInsideSystem(t *testing.T) { testutils.SetTestTimeout(t, 10*time.Second) - world, doTick := testutils.MakeWorldAndTicker(t, cardinal.WithCORS()) + world, doTick := testutils.MakeWorldAndTicker(t) assert.NilError(t, cardinal.RegisterComponent[Foo](world)) wantNumOfEntities := 10 wCtx := cardinal.TestingWorldToWorldContext(world) @@ -58,7 +58,7 @@ func TestShutdownViaSignal(t *testing.T) { // If this test is frozen then it failed to shut down, create a failure with panic. var wg sync.WaitGroup testutils.SetTestTimeout(t, 10*time.Second) - world := testutils.NewTestWorld(t, cardinal.WithCORS()) + world := testutils.NewTestWorld(t) assert.NilError(t, cardinal.RegisterComponent[Foo](world)) wantNumOfEntities := 10 world.Init(func(worldCtx cardinal.WorldContext) error { diff --git a/cardinal/config.go b/cardinal/config.go index f18df0cbf..b54e9c347 100644 --- a/cardinal/config.go +++ b/cardinal/config.go @@ -1,11 +1,22 @@ package cardinal -import "os" +import ( + "os" + + "github.com/rs/zerolog/log" +) + +const ( + CardinalModeProd = "production" + CardinalModeDev = "development" + DefaultCardinalNamespace = "world-1" + DefaultRedisPassword = "" +) type WorldConfig struct { RedisAddress string RedisPassword string - CardinalWorldId string + CardinalNamespace string CardinalPort string CardinalDeployMode string } @@ -13,16 +24,24 @@ type WorldConfig struct { func GetWorldConfig() WorldConfig { return WorldConfig{ RedisAddress: getEnv("REDIS_ADDRESS", "localhost:6379"), - RedisPassword: getEnv("REDIS_PASSWORD", ""), - CardinalWorldId: getEnv("CARDINAL_WORLD_ID", "world"), - CardinalPort: getEnv("CARDINAL_PORT", "3333"), - CardinalDeployMode: getEnv("CARDINAL_DEPLOY_MODE", "development"), + RedisPassword: getEnv("REDIS_PASSWORD", DefaultRedisPassword), + CardinalNamespace: getEnv("CARDINAL_NAMESPACE", DefaultCardinalNamespace), + CardinalPort: getEnv("CARDINAL_PORT", "4040"), + CardinalDeployMode: getEnv("CARDINAL_DEPLOY_MODE", CardinalModeDev), } } -func getEnv(key, fallback string) string { +func getEnv(key string, fallback string) string { + var value string if value, ok := os.LookupEnv(key); ok { return value } + + if key == "CARDINAL_DEPLOY_MODE" && value != CardinalModeProd && value != CardinalModeDev { + log.Logger.Warn(). + Msg("CARDINAL_DEPLOY_MODE is not set to [production/development]. Defaulting to development mode.") + return CardinalModeDev + } + return fallback } diff --git a/cardinal/ecs/benchmark/benchmark_test.go b/cardinal/ecs/benchmark/benchmark_test.go index 2b4f519ae..50f9b784a 100644 --- a/cardinal/ecs/benchmark/benchmark_test.go +++ b/cardinal/ecs/benchmark/benchmark_test.go @@ -29,7 +29,7 @@ func newWorldWithRealRedis(t testing.TB) *ecs.World { sm, err := ecb.NewManager(rs.Client) assert.NilError(t, err) - world, err := ecs.NewWorld(&rs, sm) + world, err := ecs.NewWorld(&rs, sm, ecs.Namespace("world-1")) assert.NilError(t, err) return world diff --git a/cardinal/ecs/internal/testutil/testutil.go b/cardinal/ecs/internal/testutil/testutil.go index c4faa3300..bf5796706 100644 --- a/cardinal/ecs/internal/testutil/testutil.go +++ b/cardinal/ecs/internal/testutil/testutil.go @@ -16,7 +16,7 @@ import ( "pkg.world.dev/world-engine/sign" ) -const WorldID string = "1" +const Namespace ecs.Namespace = "world" func GetRedisStorage(t *testing.T) storage.RedisStorage { s := miniredis.RunT(t) @@ -24,7 +24,7 @@ func GetRedisStorage(t *testing.T) storage.RedisStorage { Addr: s.Addr(), Password: "", // no password set DB: 0, // use default DB - }, WorldID) + }, Namespace) } // InitWorldWithRedis sets up an ecs.World using the given redis DB. ecs.NewECSWorldForTest is not used @@ -34,10 +34,10 @@ func InitWorldWithRedis(t *testing.T, s *miniredis.Miniredis) *ecs.World { Addr: s.Addr(), Password: "", // no password set DB: 0, // use default DB - }, "in-memory-world") + }, Namespace) sm, err := ecb.NewManager(rs.Client) assert.NilError(t, err) - w, err := ecs.NewWorld(&rs, sm) + w, err := ecs.NewWorld(&rs, sm, Namespace) assert.NilError(t, err) return w } @@ -95,7 +95,13 @@ func init() { func UniqueSignature(t *testing.T) *sign.Transaction { nonce++ - sig, err := sign.NewTransaction(privateKey, "some-persona-tag", "namespace", nonce, `{"some":"data"}`) + sig, err := sign.NewTransaction( + privateKey, + "some-persona-tag", + "namespace", + nonce, + `{"some":"data"}`, + ) assert.NilError(t, err) return sig } diff --git a/cardinal/ecs/storage/redis_storage.go b/cardinal/ecs/storage/redis_storage.go index 328a25042..bef66ad32 100644 --- a/cardinal/ecs/storage/redis_storage.go +++ b/cardinal/ecs/storage/redis_storage.go @@ -5,6 +5,8 @@ import ( "errors" "os" + "pkg.world.dev/world-engine/cardinal/ecs" + "github.com/redis/go-redis/v9" "github.com/rs/zerolog" ) @@ -31,18 +33,18 @@ import ( // We could end up with some issues (needs to be determined). type RedisStorage struct { - WorldID string - Client *redis.Client - Log zerolog.Logger + Namespace ecs.Namespace + Client *redis.Client + Log zerolog.Logger } type Options = redis.Options -func NewRedisStorage(options Options, worldID string) RedisStorage { +func NewRedisStorage(options Options, namespace ecs.Namespace) RedisStorage { return RedisStorage{ - WorldID: worldID, - Client: redis.NewClient(&options), - Log: zerolog.New(os.Stdout), + Namespace: namespace, + Client: redis.NewClient(&options), + Log: zerolog.New(os.Stdout), } } diff --git a/cardinal/ecs/world.go b/cardinal/ecs/world.go index f1bf8f111..8c42946e3 100644 --- a/cardinal/ecs/world.go +++ b/cardinal/ecs/world.go @@ -82,10 +82,12 @@ type World struct { } var ( - ErrMessageRegistrationMustHappenOnce = errors.New("message registration must happen exactly 1 time") - ErrStoreStateInvalid = errors.New("saved world state is not valid") - ErrDuplicateMessageName = errors.New("message names must be unique") - ErrDuplicateQueryName = errors.New("query names must be unique") + ErrMessageRegistrationMustHappenOnce = errors.New( + "message registration must happen exactly 1 time", + ) + ErrStoreStateInvalid = errors.New("saved world state is not valid") + ErrDuplicateMessageName = errors.New("message names must be unique") + ErrDuplicateQueryName = errors.New("query names must be unique") ) const ( @@ -198,7 +200,10 @@ func MustRegisterComponent[T metadata.Component](world *World) { func (w *World) GetComponentByName(name string) (metadata.ComponentMetadata, error) { componentType, exists := w.nameToComponent[name] if !exists { - return nil, fmt.Errorf("component with name %s not found. Must register component before using", name) + return nil, fmt.Errorf( + "component with name %s not found. Must register component before using", + name, + ) } return componentType, nil } @@ -265,7 +270,12 @@ func (w *World) ListMessages() ([]message.Message, error) { } // NewWorld creates a new world. -func NewWorld(nonceStore storage.NonceStorage, entityStore store.IManager, opts ...Option) (*World, error) { +func NewWorld( + nonceStore storage.NonceStorage, + entityStore store.IManager, + namespace Namespace, + opts ...Option, +) (*World, error) { logger := &ecslog.Logger{ &log.Logger, } @@ -273,7 +283,7 @@ func NewWorld(nonceStore storage.NonceStorage, entityStore store.IManager, opts w := &World{ nonceStore: nonceStore, entityStore: entityStore, - namespace: "world", + namespace: namespace, tick: 0, systems: make([]System, 0), initSystem: func(_ WorldContext) error { return nil }, @@ -336,7 +346,12 @@ func (w *World) AddTransaction(id message.TypeID, v any, sig *sign.Transaction) return tick, txHash } -func (w *World) AddEVMTransaction(id message.TypeID, v any, sig *sign.Transaction, evmTxHash string) ( +func (w *World) AddEVMTransaction( + id message.TypeID, + v any, + sig *sign.Transaction, + evmTxHash string, +) ( tick uint64, txHash message.TxHash, ) { tick = w.CurrentTick() @@ -355,7 +370,8 @@ func (w *World) Tick(_ context.Context) error { nameOfCurrentRunningSystem := nullSystemName defer func() { if panicValue := recover(); panicValue != nil { - w.Logger.Error().Msgf("Tick: %d, Current running system: %s", w.tick, nameOfCurrentRunningSystem) + w.Logger.Error(). + Msgf("Tick: %d, Current running system: %s", w.tick, nameOfCurrentRunningSystem) panic(panicValue) } }() @@ -446,7 +462,11 @@ func (w *World) setEvmResults(txs []message.TxData) { } } -func (w *World) StartGameLoop(ctx context.Context, tickStart <-chan time.Time, tickDone chan<- uint64) { +func (w *World) StartGameLoop( + ctx context.Context, + tickStart <-chan time.Time, + tickDone chan<- uint64, +) { w.Logger.Info().Msg("Game loop started") w.Logger.LogWorld(w, zerolog.InfoLevel) //todo: add links to docs related to each warning @@ -634,7 +654,11 @@ func (w *World) RecoverFromChain(ctx context.Context) error { target := tickedTxs.Epoch // tick up to target if target < w.CurrentTick() { - return fmt.Errorf("got tx for tick %d, but world is at tick %d", target, w.CurrentTick()) + return fmt.Errorf( + "got tx for tick %d, but world is at tick %d", + target, + w.CurrentTick(), + ) } for current := w.CurrentTick(); current != target; { if err = w.Tick(ctx); err != nil { diff --git a/cardinal/option.go b/cardinal/option.go index 4bb4ee821..48f7f1a95 100644 --- a/cardinal/option.go +++ b/cardinal/option.go @@ -1,12 +1,13 @@ package cardinal import ( + "time" + "github.com/alicebob/miniredis/v2" "github.com/rs/zerolog/log" ecslog "pkg.world.dev/world-engine/cardinal/ecs/log" "pkg.world.dev/world-engine/cardinal/ecs/store" "pkg.world.dev/world-engine/cardinal/events" - "time" "pkg.world.dev/world-engine/cardinal/ecs" "pkg.world.dev/world-engine/cardinal/server" @@ -82,18 +83,6 @@ func WithTickDoneChannel(ch chan<- uint64) WorldOption { } } -func WithPrettyLog() WorldOption { - return WorldOption{ - ecsOption: ecs.WithPrettyLog(), - } -} - -func WithCORS() WorldOption { - return WorldOption{ - serverOption: server.WithCORS(), - } -} - func WithStoreManager(s store.IManager) WorldOption { return WorldOption{ ecsOption: ecs.WithStoreManager(s), @@ -117,7 +106,9 @@ func withMockRedis() WorldOption { s := miniredis.NewMiniRedis() err := s.StartAddr(":6379") if err != nil { - panic("Unable to start miniredis. Make sure there is no other redis instance running on port 6379") + panic( + "Unable to start miniredis. Make sure there is no other redis instance running on port 6379", + ) } log.Logger.Debug().Msgf("miniredis started at %s", s.Addr()) diff --git a/cardinal/option_test.go b/cardinal/option_test.go index b35364748..eb9da0ea1 100644 --- a/cardinal/option_test.go +++ b/cardinal/option_test.go @@ -28,5 +28,4 @@ func TestOptionFunctionSignatures(_ *testing.T) { WithNamespace("blah") WithPort("4040") WithDisableSignatureVerification() //nolint:staticcheck //this test just looks for compile errors - WithPrettyLog() //nolint:staticcheck //this test just looks for compile errors } diff --git a/cardinal/world.go b/cardinal/world.go index ff0187abe..14961d41c 100644 --- a/cardinal/world.go +++ b/cardinal/world.go @@ -57,30 +57,42 @@ type ( func NewWorld(opts ...WorldOption) (*World, error) { ecsOptions, serverOptions, cardinalOptions := separateOptions(opts) + // Load config. Fallback value is used if it's not set. cfg := GetWorldConfig() - if cfg.CardinalDeployMode == "production" { + // Sane default options + serverOptions = append(serverOptions, server.WithCORS()) + + if cfg.CardinalDeployMode == CardinalModeProd { log.Logger.Info().Msg("Starting a new Cardinal world in production mode") - if cfg.RedisPassword == "" { + if cfg.RedisPassword == DefaultRedisPassword { return nil, errors.New("redis password is required in production") } - } else { - if cfg.CardinalDeployMode != "development" { - log.Logger.Warn().Msg("CARDINAL_DEPLOY_MODE is unrecognized. Defaulting to development mode") + if cfg.CardinalNamespace == DefaultCardinalNamespace { + return nil, errors.New( + "cardinal namespace can't be the default value in production to avoid replay attack", + ) } + } else { log.Logger.Info().Msg("Starting a new Cardinal world in development mode") + ecsOptions = append(ecsOptions, ecs.WithPrettyLog()) } + redisStore := storage.NewRedisStorage(storage.Options{ Addr: cfg.RedisAddress, Password: cfg.RedisPassword, DB: 0, // use default DB - }, cfg.CardinalWorldId) + }, ecs.Namespace(cfg.CardinalNamespace)) storeManager, err := ecb.NewManager(redisStore.Client) if err != nil { return nil, err } - ecsWorld, err := ecs.NewWorld(&redisStore, storeManager, ecsOptions...) + ecsWorld, err := ecs.NewWorld( + &redisStore, + storeManager, + ecs.Namespace(cfg.CardinalNamespace), + ecsOptions...) if err != nil { return nil, err } @@ -91,6 +103,8 @@ func NewWorld(opts ...WorldOption) (*World, error) { endStartGame: make(chan bool), } world.isGameRunning.Store(false) + + // Apply options for _, opt := range cardinalOptions { opt(world) } @@ -101,7 +115,7 @@ func NewWorld(opts ...WorldOption) (*World, error) { // NewMockWorld creates a World object that uses miniredis as the storage layer suitable for local development. // If you are creating a World for unit tests, use NewTestWorld. func NewMockWorld(opts ...WorldOption) (*World, error) { - world, err := NewWorld(withMockRedis(), opts...) + world, err := NewWorld(append(opts, withMockRedis())...) if err != nil { return world, err } @@ -181,7 +195,10 @@ func (w *World) StartGame() error { } eventHub := events.CreateWebSocketEventHub() w.instance.SetEventHub(eventHub) - eventBuilder := events.CreateNewWebSocketBuilder("/events", events.CreateWebSocketEventHandler(eventHub)) + eventBuilder := events.CreateNewWebSocketBuilder( + "/events", + events.CreateWebSocketEventHandler(eventHub), + ) handler, err := server.NewHandler(w.instance, eventBuilder, w.serverOptions...) if err != nil { return err @@ -193,7 +210,8 @@ func (w *World) StartGame() error { if !errors.Is(err, evm.ErrNoEVMTypes) { return err } - w.instance.Logger.Debug().Msg("no EVM messages or queries specified. EVM server will not run") + w.instance.Logger.Debug(). + Msg("no EVM messages or queries specified. EVM server will not run") } else { w.instance.Logger.Debug().Msg("running world with EVM server") err = w.evmServer.Serve() From 55e94f5b19f73501fa65e16d31819b86412b743c Mon Sep 17 00:00:00 2001 From: smsunarto Date: Fri, 10 Nov 2023 17:04:23 -0800 Subject: [PATCH 07/14] fix test --- cardinal/ecs/internal/testutil/testutil.go | 4 ++-- cardinal/ecs/storage/redis_storage.go | 6 ++---- cardinal/world.go | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/cardinal/ecs/internal/testutil/testutil.go b/cardinal/ecs/internal/testutil/testutil.go index bf5796706..56d4de8cd 100644 --- a/cardinal/ecs/internal/testutil/testutil.go +++ b/cardinal/ecs/internal/testutil/testutil.go @@ -16,7 +16,7 @@ import ( "pkg.world.dev/world-engine/sign" ) -const Namespace ecs.Namespace = "world" +const Namespace string = "world" func GetRedisStorage(t *testing.T) storage.RedisStorage { s := miniredis.RunT(t) @@ -37,7 +37,7 @@ func InitWorldWithRedis(t *testing.T, s *miniredis.Miniredis) *ecs.World { }, Namespace) sm, err := ecb.NewManager(rs.Client) assert.NilError(t, err) - w, err := ecs.NewWorld(&rs, sm, Namespace) + w, err := ecs.NewWorld(&rs, sm, ecs.Namespace(Namespace)) assert.NilError(t, err) return w } diff --git a/cardinal/ecs/storage/redis_storage.go b/cardinal/ecs/storage/redis_storage.go index bef66ad32..989efde57 100644 --- a/cardinal/ecs/storage/redis_storage.go +++ b/cardinal/ecs/storage/redis_storage.go @@ -5,8 +5,6 @@ import ( "errors" "os" - "pkg.world.dev/world-engine/cardinal/ecs" - "github.com/redis/go-redis/v9" "github.com/rs/zerolog" ) @@ -33,14 +31,14 @@ import ( // We could end up with some issues (needs to be determined). type RedisStorage struct { - Namespace ecs.Namespace + Namespace string Client *redis.Client Log zerolog.Logger } type Options = redis.Options -func NewRedisStorage(options Options, namespace ecs.Namespace) RedisStorage { +func NewRedisStorage(options Options, namespace string) RedisStorage { return RedisStorage{ Namespace: namespace, Client: redis.NewClient(&options), diff --git a/cardinal/world.go b/cardinal/world.go index 14961d41c..02300a0ab 100644 --- a/cardinal/world.go +++ b/cardinal/world.go @@ -82,7 +82,7 @@ func NewWorld(opts ...WorldOption) (*World, error) { Addr: cfg.RedisAddress, Password: cfg.RedisPassword, DB: 0, // use default DB - }, ecs.Namespace(cfg.CardinalNamespace)) + }, cfg.CardinalNamespace) storeManager, err := ecb.NewManager(redisStore.Client) if err != nil { return nil, err From 4816f6788bc5e2615d6eb91fac5c252467664c35 Mon Sep 17 00:00:00 2001 From: smsunarto Date: Fri, 10 Nov 2023 21:39:28 -0800 Subject: [PATCH 08/14] unfuck e2e --- internal/e2e/tester/cardinal/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/e2e/tester/cardinal/main.go b/internal/e2e/tester/cardinal/main.go index c90bce169..2079164cc 100644 --- a/internal/e2e/tester/cardinal/main.go +++ b/internal/e2e/tester/cardinal/main.go @@ -25,7 +25,7 @@ func main() { options = append(options, cardinal.WithAdapter(setupAdapter())) } - world, err := cardinal.NewWorld(options...) + world, err := cardinal.NewWorld(os.Getenv("REDIS_ADDR"), "", options...) if err != nil { log.Fatal(err) } From b5407668c8f599f82d230d9b22a24c10ec3104d9 Mon Sep 17 00:00:00 2001 From: smsunarto Date: Fri, 10 Nov 2023 21:55:23 -0800 Subject: [PATCH 09/14] fix cardinal mode fallback --- cardinal/config.go | 36 ++++++++++++++-------------- cardinal/testutils/test_utils.go | 40 +++++++++++++++++++++++++------- cardinal/world.go | 2 +- 3 files changed, 51 insertions(+), 27 deletions(-) diff --git a/cardinal/config.go b/cardinal/config.go index b54e9c347..f97b3fa06 100644 --- a/cardinal/config.go +++ b/cardinal/config.go @@ -14,34 +14,34 @@ const ( ) type WorldConfig struct { - RedisAddress string - RedisPassword string - CardinalNamespace string - CardinalPort string - CardinalDeployMode string + RedisAddress string + RedisPassword string + CardinalNamespace string + CardinalPort string + CardinalMode string } func GetWorldConfig() WorldConfig { return WorldConfig{ - RedisAddress: getEnv("REDIS_ADDRESS", "localhost:6379"), - RedisPassword: getEnv("REDIS_PASSWORD", DefaultRedisPassword), - CardinalNamespace: getEnv("CARDINAL_NAMESPACE", DefaultCardinalNamespace), - CardinalPort: getEnv("CARDINAL_PORT", "4040"), - CardinalDeployMode: getEnv("CARDINAL_DEPLOY_MODE", CardinalModeDev), + RedisAddress: getEnv("REDIS_ADDRESS", "localhost:6379"), + RedisPassword: getEnv("REDIS_PASSWORD", DefaultRedisPassword), + CardinalNamespace: getEnv("CARDINAL_NAMESPACE", DefaultCardinalNamespace), + CardinalPort: getEnv("CARDINAL_PORT", "4040"), + CardinalMode: getEnv("CARDINAL_MODE", CardinalModeDev), } } func getEnv(key string, fallback string) string { - var value string - if value, ok := os.LookupEnv(key); ok { + value, ok := os.LookupEnv(key) + if ok { + // Validate CARDINAL_DEPLOY_MODE + if key == "CARDINAL_MODE" && value != CardinalModeProd && value != CardinalModeDev { + log.Logger.Warn(). + Msg("CARDINAL_DEPLOY_MODE is not set to [production/development]. Defaulting to development mode.") + return CardinalModeDev + } return value } - if key == "CARDINAL_DEPLOY_MODE" && value != CardinalModeProd && value != CardinalModeDev { - log.Logger.Warn(). - Msg("CARDINAL_DEPLOY_MODE is not set to [production/development]. Defaulting to development mode.") - return CardinalModeDev - } - return fallback } diff --git a/cardinal/testutils/test_utils.go b/cardinal/testutils/test_utils.go index 330c896d8..0fade2fd2 100644 --- a/cardinal/testutils/test_utils.go +++ b/cardinal/testutils/test_utils.go @@ -8,11 +8,12 @@ import ( "errors" "fmt" "net/http" - "pkg.world.dev/world-engine/cardinal/ecs" "sync" "testing" "time" + "pkg.world.dev/world-engine/cardinal/ecs" + "gotest.tools/v3/assert" "github.com/ethereum/go-ethereum/crypto" @@ -22,12 +23,19 @@ import ( "pkg.world.dev/world-engine/sign" ) -func MakeTestTransactionHandler(t *testing.T, world *ecs.World, opts ...server.Option) *TestTransactionHandler { +func MakeTestTransactionHandler( + t *testing.T, + world *ecs.World, + opts ...server.Option, +) *TestTransactionHandler { port := "4040" opts = append(opts, server.WithPort(port)) eventHub := events.CreateWebSocketEventHub() world.SetEventHub(eventHub) - eventBuilder := events.CreateNewWebSocketBuilder("/events", events.CreateWebSocketEventHandler(eventHub)) + eventBuilder := events.CreateNewWebSocketBuilder( + "/events", + events.CreateWebSocketEventHandler(eventHub), + ) txh, err := server.NewHandler(world, eventBuilder, opts...) assert.NilError(t, err) @@ -56,7 +64,11 @@ func MakeTestTransactionHandler(t *testing.T, world *ecs.World, opts ...server.O healthURL := host + healthPath start := time.Now() for { - assert.Check(t, time.Since(start) < time.Second, "timeout while waiting for a healthy server") + assert.Check( + t, + time.Since(start) < time.Second, + "timeout while waiting for a healthy server", + ) //nolint:noctx,bodyclose // its for a test. resp, err := http.Get("http://" + healthURL) if err == nil && resp.StatusCode == 200 { @@ -179,8 +191,12 @@ func AddTransactionToWorldByAnyTransaction( } } if !found { - panic(fmt.Sprintf("cannot find transaction %q in registered transactions. Did you register it?", - cardinalTx.Convert().Name())) + panic( + fmt.Sprintf( + "cannot find transaction %q in registered transactions. Did you register it?", + cardinalTx.Convert().Name(), + ), + ) } _, _ = ecsWorld.AddTransaction(txID, value, tx) @@ -189,10 +205,18 @@ func AddTransactionToWorldByAnyTransaction( // MakeWorldAndTicker sets up a cardinal.World as well as a function that can execute one game tick. The *cardinal.World // will be automatically started when doTick is called for the first time. The cardinal.World will be shut down at the // end of the test. If doTick takes longer than 5 seconds to run, t.Fatal will be called. -func MakeWorldAndTicker(t *testing.T, opts ...cardinal.WorldOption) (world *cardinal.World, doTick func()) { +func MakeWorldAndTicker( + t *testing.T, + opts ...cardinal.WorldOption, +) (world *cardinal.World, doTick func()) { startTickCh, doneTickCh := make(chan time.Time), make(chan uint64) eventHub := events.CreateWebSocketEventHub() - opts = append(opts, cardinal.WithTickChannel(startTickCh), cardinal.WithTickDoneChannel(doneTickCh), cardinal.WithEventHub(eventHub)) + opts = append( + opts, + cardinal.WithTickChannel(startTickCh), + cardinal.WithTickDoneChannel(doneTickCh), + cardinal.WithEventHub(eventHub), + ) world = NewTestWorld(t, opts...) // Shutdown any world resources. This will be called whether the world has been started or not. diff --git a/cardinal/world.go b/cardinal/world.go index 02300a0ab..273fd030f 100644 --- a/cardinal/world.go +++ b/cardinal/world.go @@ -63,7 +63,7 @@ func NewWorld(opts ...WorldOption) (*World, error) { // Sane default options serverOptions = append(serverOptions, server.WithCORS()) - if cfg.CardinalDeployMode == CardinalModeProd { + if cfg.CardinalMode == CardinalModeProd { log.Logger.Info().Msg("Starting a new Cardinal world in production mode") if cfg.RedisPassword == DefaultRedisPassword { return nil, errors.New("redis password is required in production") From 252fdfe84454ecf8ff1bfc92e6f837a8eb5aaa04 Mon Sep 17 00:00:00 2001 From: smsunarto Date: Fri, 10 Nov 2023 22:20:51 -0800 Subject: [PATCH 10/14] remove options that should be set from env var instead --- cardinal/cardinal_test.go | 10 +++++++++- cardinal/ecs/options.go | 6 ------ cardinal/option.go | 16 ---------------- cardinal/option_test.go | 7 +++++-- cardinal/server/option.go | 6 ------ cardinal/testutils/test_utils.go | 4 +--- 6 files changed, 15 insertions(+), 34 deletions(-) diff --git a/cardinal/cardinal_test.go b/cardinal/cardinal_test.go index e35330588..2a103779f 100644 --- a/cardinal/cardinal_test.go +++ b/cardinal/cardinal_test.go @@ -21,8 +21,16 @@ type Foo struct{} func (Foo) Name() string { return "foo" } func TestNewWorld(t *testing.T) { - _, err := cardinal.NewWorld(cardinal.WithNamespace("testnamespace")) + world, err := cardinal.NewWorld() assert.NilError(t, err) + assert.Equal(t, world.Instance().Namespace(), cardinal.DefaultCardinalNamespace) +} + +func TestNewWorldWithCustomNamespace(t *testing.T) { + t.Setenv("CARDINAL_NAMESPACE", "custom-namespace") + world, err := cardinal.NewWorld() + assert.NilError(t, err) + assert.Equal(t, world.Instance().Namespace(), "custom-namespace") } func TestCanQueryInsideSystem(t *testing.T) { diff --git a/cardinal/ecs/options.go b/cardinal/ecs/options.go index 395f1e2e6..94ebaec92 100644 --- a/cardinal/ecs/options.go +++ b/cardinal/ecs/options.go @@ -26,12 +26,6 @@ func WithReceiptHistorySize(size int) Option { } } -func WithNamespace(ns string) Option { - return func(w *World) { - w.namespace = Namespace(ns) - } -} - func WithPrettyLog() Option { return func(world *World) { prettyLogger := log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) diff --git a/cardinal/option.go b/cardinal/option.go index 48f7f1a95..c8fae150a 100644 --- a/cardinal/option.go +++ b/cardinal/option.go @@ -38,22 +38,6 @@ func WithReceiptHistorySize(size int) WorldOption { } } -// WithNamespace sets the World's namespace. The default is "world". The namespace is used in the transaction -// signing process. -func WithNamespace(namespace string) WorldOption { - return WorldOption{ - ecsOption: ecs.WithNamespace(namespace), - } -} - -// WithPort specifies the port for the World's HTTP server. If omitted, the environment variable CARDINAL_PORT -// will be used, and if that is unset, port 4040 will be used. -func WithPort(port string) WorldOption { - return WorldOption{ - serverOption: server.WithPort(port), - } -} - // WithDisableSignatureVerification disables signature verification for the HTTP server. This should only be // used for local development. func WithDisableSignatureVerification() WorldOption { diff --git a/cardinal/option_test.go b/cardinal/option_test.go index eb9da0ea1..0dfba0e6a 100644 --- a/cardinal/option_test.go +++ b/cardinal/option_test.go @@ -25,7 +25,10 @@ func TestOptionFunctionSignatures(_ *testing.T) { // public facing functions was changed. WithAdapter(&DummyAdapter{}) WithReceiptHistorySize(1) - WithNamespace("blah") - WithPort("4040") + WithTickChannel(nil) + WithTickDoneChannel(nil) + WithStoreManager(nil) + WithEventHub(nil) + WithLoggingEventHub(nil) WithDisableSignatureVerification() //nolint:staticcheck //this test just looks for compile errors } diff --git a/cardinal/server/option.go b/cardinal/server/option.go index dfb01f39d..3c2ca417c 100644 --- a/cardinal/server/option.go +++ b/cardinal/server/option.go @@ -4,12 +4,6 @@ import "pkg.world.dev/world-engine/cardinal/shard" type Option func(th *Handler) -func WithPort(port string) Option { - return func(th *Handler) { - th.Port = port - } -} - func DisableSignatureVerification() Option { return func(th *Handler) { th.disableSigVerification = true diff --git a/cardinal/testutils/test_utils.go b/cardinal/testutils/test_utils.go index 0fade2fd2..3738676e1 100644 --- a/cardinal/testutils/test_utils.go +++ b/cardinal/testutils/test_utils.go @@ -28,8 +28,6 @@ func MakeTestTransactionHandler( world *ecs.World, opts ...server.Option, ) *TestTransactionHandler { - port := "4040" - opts = append(opts, server.WithPort(port)) eventHub := events.CreateWebSocketEventHub() world.SetEventHub(eventHub) eventBuilder := events.CreateNewWebSocketBuilder( @@ -60,7 +58,7 @@ func MakeTestTransactionHandler( _ = gameObject.Shutdown() }) - host := "localhost:" + port + host := "localhost:4040" healthURL := host + healthPath start := time.Now() for { From e97ff3b3b31b1fc86fb1dc4fdba342c4b9062a0a Mon Sep 17 00:00:00 2001 From: smsunarto Date: Fri, 10 Nov 2023 22:31:57 -0800 Subject: [PATCH 11/14] fix logging test --- cardinal/cardinal_test.go | 4 ++-- cardinal/ecs/log/log_test.go | 4 ++++ cardinal/events/events_test.go | 4 ++++ cardinal/testutils/{test_utils.go => testutils.go} | 0 cardinal/testutils/world.go | 2 ++ 5 files changed, 12 insertions(+), 2 deletions(-) rename cardinal/testutils/{test_utils.go => testutils.go} (100%) diff --git a/cardinal/cardinal_test.go b/cardinal/cardinal_test.go index 2a103779f..55fab9f46 100644 --- a/cardinal/cardinal_test.go +++ b/cardinal/cardinal_test.go @@ -23,14 +23,14 @@ func (Foo) Name() string { return "foo" } func TestNewWorld(t *testing.T) { world, err := cardinal.NewWorld() assert.NilError(t, err) - assert.Equal(t, world.Instance().Namespace(), cardinal.DefaultCardinalNamespace) + assert.Equal(t, string(world.Instance().Namespace()), cardinal.DefaultCardinalNamespace) } func TestNewWorldWithCustomNamespace(t *testing.T) { t.Setenv("CARDINAL_NAMESPACE", "custom-namespace") world, err := cardinal.NewWorld() assert.NilError(t, err) - assert.Equal(t, world.Instance().Namespace(), "custom-namespace") + assert.Equal(t, string(world.Instance().Namespace()), "custom-namespace") } func TestCanQueryInsideSystem(t *testing.T) { diff --git a/cardinal/ecs/log/log_test.go b/cardinal/ecs/log/log_test.go index a1c732cef..6dcdb34d3 100644 --- a/cardinal/ecs/log/log_test.go +++ b/cardinal/ecs/log/log_test.go @@ -79,6 +79,10 @@ func TestWarningLogIfDuplicateSystemRegistered(t *testing.T) { func TestWorldLogger(t *testing.T) { w := testutils.NewTestWorld(t).Instance() + + // testutils.NewTestWorld sets the log level to error, so we need to set it to zerolog.DebugLevel to pass this test + zerolog.SetGlobalLevel(zerolog.DebugLevel) + // replaces internal Logger with one that logs to the buf variable above. var buf bytes.Buffer bufLogger := zerolog.New(&buf) diff --git a/cardinal/events/events_test.go b/cardinal/events/events_test.go index eaa087e93..fecd7e90e 100644 --- a/cardinal/events/events_test.go +++ b/cardinal/events/events_test.go @@ -156,6 +156,10 @@ func TestEventHubLogger(t *testing.T) { &bufLogger, } w := testutils.NewTestWorld(t, cardinal.WithLoggingEventHub(&cardinalLogger)).Instance() + + // testutils.NewTestWorld sets the log level to error, so we need to set it to zerolog.DebugLevel to pass this test + zerolog.SetGlobalLevel(zerolog.DebugLevel) + numberToTest := 5 for i := 0; i < numberToTest; i++ { w.RegisterSystem(func(wCtx ecs.WorldContext) error { diff --git a/cardinal/testutils/test_utils.go b/cardinal/testutils/testutils.go similarity index 100% rename from cardinal/testutils/test_utils.go rename to cardinal/testutils/testutils.go diff --git a/cardinal/testutils/world.go b/cardinal/testutils/world.go index 1936dfd40..6e992919c 100644 --- a/cardinal/testutils/world.go +++ b/cardinal/testutils/world.go @@ -2,6 +2,7 @@ package testutils import ( "github.com/alicebob/miniredis/v2" + "github.com/rs/zerolog" "gotest.tools/v3/assert" "pkg.world.dev/world-engine/cardinal" "testing" @@ -11,6 +12,7 @@ import ( // Relevant resources are automatically cleaned up at the completion of each test. func NewTestWorld(t testing.TB, opts ...cardinal.WorldOption) *cardinal.World { // Init testing environment + zerolog.SetGlobalLevel(zerolog.ErrorLevel) s := miniredis.RunT(t) t.Setenv("CARDINAL_DEPLOY_MODE", "development") t.Setenv("REDIS_ADDRESS", s.Addr()) From 2c504fb59733f59e993d7c9d0250bcf35a9514dd Mon Sep 17 00:00:00 2001 From: smsunarto Date: Mon, 13 Nov 2023 12:18:44 -0800 Subject: [PATCH 12/14] address comments --- cardinal/cardinal_test.go | 2 +- cardinal/config.go | 17 +++++++++-------- cardinal/ecs/benchmark/benchmark_test.go | 3 ++- cardinal/ecs/world_test.go | 2 +- cardinal/world.go | 4 ++-- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/cardinal/cardinal_test.go b/cardinal/cardinal_test.go index 55fab9f46..33a274106 100644 --- a/cardinal/cardinal_test.go +++ b/cardinal/cardinal_test.go @@ -23,7 +23,7 @@ func (Foo) Name() string { return "foo" } func TestNewWorld(t *testing.T) { world, err := cardinal.NewWorld() assert.NilError(t, err) - assert.Equal(t, string(world.Instance().Namespace()), cardinal.DefaultCardinalNamespace) + assert.Equal(t, string(world.Instance().Namespace()), cardinal.DefaultNamespace) } func TestNewWorldWithCustomNamespace(t *testing.T) { diff --git a/cardinal/config.go b/cardinal/config.go index f97b3fa06..c84dd38d4 100644 --- a/cardinal/config.go +++ b/cardinal/config.go @@ -7,10 +7,11 @@ import ( ) const ( - CardinalModeProd = "production" - CardinalModeDev = "development" - DefaultCardinalNamespace = "world-1" - DefaultRedisPassword = "" + ModeProd = "production" + ModeDev = "development" + DefaultMode = ModeDev + DefaultNamespace = "world-1" + DefaultRedisPassword = "" ) type WorldConfig struct { @@ -25,9 +26,9 @@ func GetWorldConfig() WorldConfig { return WorldConfig{ RedisAddress: getEnv("REDIS_ADDRESS", "localhost:6379"), RedisPassword: getEnv("REDIS_PASSWORD", DefaultRedisPassword), - CardinalNamespace: getEnv("CARDINAL_NAMESPACE", DefaultCardinalNamespace), + CardinalNamespace: getEnv("CARDINAL_NAMESPACE", DefaultNamespace), CardinalPort: getEnv("CARDINAL_PORT", "4040"), - CardinalMode: getEnv("CARDINAL_MODE", CardinalModeDev), + CardinalMode: getEnv("CARDINAL_MODE", DefaultMode), } } @@ -35,10 +36,10 @@ func getEnv(key string, fallback string) string { value, ok := os.LookupEnv(key) if ok { // Validate CARDINAL_DEPLOY_MODE - if key == "CARDINAL_MODE" && value != CardinalModeProd && value != CardinalModeDev { + if key == "CARDINAL_MODE" && value != ModeProd && value != ModeDev { log.Logger.Warn(). Msg("CARDINAL_DEPLOY_MODE is not set to [production/development]. Defaulting to development mode.") - return CardinalModeDev + return ModeDev } return value } diff --git a/cardinal/ecs/benchmark/benchmark_test.go b/cardinal/ecs/benchmark/benchmark_test.go index 50f9b784a..21664c143 100644 --- a/cardinal/ecs/benchmark/benchmark_test.go +++ b/cardinal/ecs/benchmark/benchmark_test.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "github.com/rs/zerolog" + "pkg.world.dev/world-engine/cardinal" "testing" "gotest.tools/v3/assert" @@ -29,7 +30,7 @@ func newWorldWithRealRedis(t testing.TB) *ecs.World { sm, err := ecb.NewManager(rs.Client) assert.NilError(t, err) - world, err := ecs.NewWorld(&rs, sm, ecs.Namespace("world-1")) + world, err := ecs.NewWorld(&rs, sm, cardinal.DefaultNamespace) assert.NilError(t, err) return world diff --git a/cardinal/ecs/world_test.go b/cardinal/ecs/world_test.go index 7f02cc5fa..52a434881 100644 --- a/cardinal/ecs/world_test.go +++ b/cardinal/ecs/world_test.go @@ -168,7 +168,7 @@ func TestAddSystems(t *testing.T) { } w := testutils.NewTestWorld(t).Instance() - w.RegiterSystems(sys, sys, sys) + w.RegisterSystems(sys, sys, sys) err := w.LoadGameState() assert.NilError(t, err) diff --git a/cardinal/world.go b/cardinal/world.go index 273fd030f..61bf85028 100644 --- a/cardinal/world.go +++ b/cardinal/world.go @@ -63,12 +63,12 @@ func NewWorld(opts ...WorldOption) (*World, error) { // Sane default options serverOptions = append(serverOptions, server.WithCORS()) - if cfg.CardinalMode == CardinalModeProd { + if cfg.CardinalMode == ModeProd { log.Logger.Info().Msg("Starting a new Cardinal world in production mode") if cfg.RedisPassword == DefaultRedisPassword { return nil, errors.New("redis password is required in production") } - if cfg.CardinalNamespace == DefaultCardinalNamespace { + if cfg.CardinalNamespace == DefaultNamespace { return nil, errors.New( "cardinal namespace can't be the default value in production to avoid replay attack", ) From 991914e9e2e882e9780d9674335afc8887dc6ca4 Mon Sep 17 00:00:00 2001 From: smsunarto Date: Mon, 13 Nov 2023 12:24:01 -0800 Subject: [PATCH 13/14] fix tests --- cardinal/ecs/log/log_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cardinal/ecs/log/log_test.go b/cardinal/ecs/log/log_test.go index 6dcdb34d3..bd5048372 100644 --- a/cardinal/ecs/log/log_test.go +++ b/cardinal/ecs/log/log_test.go @@ -63,7 +63,7 @@ func testSystemWarningTrigger(wCtx ecs.WorldContext) error { } func TestWarningLogIfDuplicateSystemRegistered(t *testing.T) { - w := ecs.NewTestWorld(t) + w := testutils.NewTestWorld(t).Instance() // replaces internal Logger with one that logs to the buf variable above. var buf bytes.Buffer bufLogger := zerolog.New(&buf) From c97edae2f1639d6b8241029deab124b44c1aa7fa Mon Sep 17 00:00:00 2001 From: smsunarto Date: Mon, 13 Nov 2023 12:38:35 -0800 Subject: [PATCH 14/14] fix tests again --- cardinal/ecs/log/log_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cardinal/ecs/log/log_test.go b/cardinal/ecs/log/log_test.go index bd5048372..34656eaa2 100644 --- a/cardinal/ecs/log/log_test.go +++ b/cardinal/ecs/log/log_test.go @@ -64,6 +64,7 @@ func testSystemWarningTrigger(wCtx ecs.WorldContext) error { func TestWarningLogIfDuplicateSystemRegistered(t *testing.T) { w := testutils.NewTestWorld(t).Instance() + zerolog.SetGlobalLevel(zerolog.DebugLevel) // replaces internal Logger with one that logs to the buf variable above. var buf bytes.Buffer bufLogger := zerolog.New(&buf)