diff --git a/CHANGELOG.md b/CHANGELOG.md index bba000d..065ee4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# v1.18.3 (Mon Aug 08 2022) + +#### Enhancement + +- Redis fixtures support +- Custom loader support if using gonkey as a library with a FixtureLoader configuration attribute + +#### Authors: 1 + +- Alexander Nemtarev [#178](https://github.com/lamoda/gonkey/pull/176) ([@anemtarev](https://github.com/anemtarev)) + # v1.18.2 (Fri Jul 08 2022) #### πŸ› Bug Fix diff --git a/README-ru.md b/README-ru.md index 56c811c..75e77c8 100644 --- a/README-ru.md +++ b/README-ru.md @@ -6,7 +6,7 @@ Gonkey протСстируСт ваши сСрвисы, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡ ΠΈΡ… - Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ с REST/JSON API - ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° API сСрвиса Π½Π° соотвСтствиС OpenAPI-спСкС -- Π·Π°ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ Π‘Π” сСрвиса Π΄Π°Π½Π½Ρ‹ΠΌΠΈ ΠΈΠ· фикстур (поддСрТиваСтся PostgreSQL, MySQL, Aerospike) +- Π·Π°ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ Π‘Π” сСрвиса Π΄Π°Π½Π½Ρ‹ΠΌΠΈ ΠΈΠ· фикстур (поддСрТиваСтся PostgreSQL, MySQL, Aerospike, Redis) - ΠΌΠΎΠΊΠΈ для ΠΈΠΌΠΈΡ‚Π°Ρ†ΠΈΠΈ Π²Π½Π΅ΡˆΠ½ΠΈΡ… сСрвисов - ΠΌΠΎΠΆΠ½ΠΎ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ ΠΊ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Ρƒ ΠΊΠ°ΠΊ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΡƒ ΠΈ Π·Π°ΠΏΡƒΡΠΊΠ°Ρ‚ΡŒ вмСстС с ΡŽΠ½ΠΈΡ‚-тСстами - запись Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π° тСстов Π² Π²ΠΈΠ΄Π΅ ΠΎΡ‚Ρ‡Π΅Ρ‚Π° [Allure](http://allure.qatools.ru/) @@ -33,6 +33,7 @@ Gonkey протСстируСт ваши сСрвисы, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡ ΠΈΡ… - [БвязываниС записСй](#связываниС-записСй) - [ВыраТСния](#выраТСния) - [Aerospike](#aerospike) + - [Redis](#redis) - [Моки](#ΠΌΠΎΠΊΠΈ) - [Запуск ΠΌΠΎΠΊΠΎΠ² ΠΏΡ€ΠΈ использовании gonkey ΠΊΠ°ΠΊ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ](#запуск-ΠΌΠΎΠΊΠΎΠ²-ΠΏΡ€ΠΈ-использовании-gonkey-ΠΊΠ°ΠΊ-Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ) - [ОписаниС ΠΌΠΎΠΊΠΎΠ² Π² Ρ„Π°ΠΉΠ»Π΅ с тСстом](#описаниС-ΠΌΠΎΠΊΠΎΠ²-Π²-Ρ„Π°ΠΉΠ»Π΅-с-тСстом) @@ -58,9 +59,10 @@ Gonkey протСстируСт ваши сСрвисы, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡ ΠΈΡ… - `-spec <...>` ΠΏΡƒΡ‚ΡŒ ΠΊ Ρ„Π°ΠΉΠ»Ρƒ ΠΈΠ»ΠΈ URL со swagger-спСцификациСй сСрвиса - `-host <...>` хост:ΠΏΠΎΡ€Ρ‚ сСрвиса - `-tests <...>` Ρ„Π°ΠΉΠ» ΠΈΠ»ΠΈ дирСктория с тСстами -- `-db-type <...>` - Ρ‚ΠΈΠΏ Π±Π°Π·Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ…. Π’ Π΄Π°Π½Π½Ρ‹ΠΉ ΠΌΠΎΠΌΠ΅Π½Ρ‚ поддСрТиваСтся PostgreSQL ΠΈ Aerospike. +- `-db-type <...>` - Ρ‚ΠΈΠΏ Π±Π°Π·Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ…. Π’ Π΄Π°Π½Π½Ρ‹ΠΉ ΠΌΠΎΠΌΠ΅Π½Ρ‚ поддСрТиваСтся PostgreSQL, Aerospike, Redis. - `-db_dsn <...>` dsn для вашСй тСстовой SQL Π±Π°Π·Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ… (Π±Π΄ Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΡ‡ΠΈΡ‰Π΅Π½Π° ΠΏΠ΅Ρ€Π΅Π΄ Π½Π°ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ΠΌ!), поддСрТиваСтся Ρ‚ΠΎΠ»ΡŒΠΊΠΎ PostgreSQL - `-aerospike_host <...>` ΠΏΡ€ΠΈ использовании Aerospike - URL для ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊ Π½Π΅ΠΌΡƒ Π² Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Π΅ `host:port/namespace` +- `-redis_addr <...>` ΠΏΡ€ΠΈ использовании Redis - адрСс для ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊ Redis Π² Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Π΅ `host:port` - `-fixtures <...>` дирСктория с вашими фикстурами - `-allure` Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ allure-ΠΎΡ‚Ρ‡Π΅Ρ‚ - `-v` ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½Ρ‹ΠΉ Π²Ρ‹Π²ΠΎΠ΄ @@ -86,35 +88,90 @@ import ( Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ с тСстом. ```go +package test + +import ( + "testing" + + "github.com/lamoda/gonkey/fixtures" + "github.com/lamoda/gonkey/mocks" + "github.com/lamoda/gonkey/runner" +) + func TestFuncCases(t *testing.T) { - // ΠΏΡ€ΠΎΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠΉΡ‚Π΅ ΠΌΠΎΠΊΠΈ, Ссли Π½ΡƒΠΆΠ½ΠΎ (ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½Π΅Π΅ - Π½ΠΈΠΆΠ΅) - //m := mocks.NewNop(...) - - // ΠΏΡ€ΠΎΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠΉΡ‚Π΅ Π±Π°Π·Ρƒ для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ фикстур, Ссли Π½ΡƒΠΆΠ½ΠΎ (ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½Π΅Π΅ - Π½ΠΈΠΆΠ΅) - //db := ... - - // ΠΏΡ€ΠΎΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠΉΡ‚Π΅ Aerospike для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ фикстур, Ссли Π½ΡƒΠΆΠ½ΠΎ (ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½Π΅Π΅ - Π½ΠΈΠΆΠ΅) - //aerospikeClient := ... - - // создайтС экзСмпляр сСрвСра вашСго прилоТСния - srv := server.NewServer() - defer srv.Close() - - // запуститС Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ тСстов ΠΈΠ· Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ cases с записью Π² ΠΎΡ‚Ρ‡Π΅Ρ‚ Allure - runner.RunWithTesting(t, &runner.RunWithTestingParams{ - Server: srv, - TestsDir: "cases", - Mocks: m, - DB: db, - Aerospike: runner.Aerospike{ - Client: aerospikeClient, - Namespace: "test", - } - // Π’ΠΈΠΏ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌΠΎΠΉ Π±Π°Π·Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ…, Π²ΠΎΠ·ΠΌΠΎΠΆΠ½Ρ‹Π΅ значСния fixtures.Postgres, fixtures.Mysql, fixtures.Aerospike - // Если Π² ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ DB Π½Π΅ пустой, Π° Π΄Π°Π½Π½Ρ‹ΠΉ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ Π½Π΅ Π½Π°Π·Π½Π°Ρ‡Π΅Π½, Π±ΡƒΠ΄Π΅Ρ‚ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒΡΡ Ρ‚ΠΈΠΏ Π±Π΄ fixtures.Postgresql - DbType: fixtures.Postgres, - FixturesDir: "fixtures", - }) + // ΠΏΡ€ΠΎΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠΉΡ‚Π΅ ΠΌΠΎΠΊΠΈ, Ссли Π½ΡƒΠΆΠ½ΠΎ (ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½Π΅Π΅ - Π½ΠΈΠΆΠ΅) + // m := mocks.NewNop(...) + + // ΠΏΡ€ΠΎΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠΉΡ‚Π΅ Π±Π°Π·Ρƒ для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ фикстур, Ссли Π½ΡƒΠΆΠ½ΠΎ (ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½Π΅Π΅ - Π½ΠΈΠΆΠ΅) + // db := ... + + // ΠΏΡ€ΠΎΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠΉΡ‚Π΅ Aerospike для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ фикстур, Ссли Π½ΡƒΠΆΠ½ΠΎ (ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½Π΅Π΅ - Π½ΠΈΠΆΠ΅) + // aerospikeClient := ... + + // создайтС экзСмпляр сСрвСра вашСго прилоТСния + srv := server.NewServer() + defer srv.Close() + + // запуститС Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ тСстов ΠΈΠ· Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ cases с записью Π² ΠΎΡ‚Ρ‡Π΅Ρ‚ Allure + runner.RunWithTesting(t, &runner.RunWithTestingParams{ + Server: srv, + TestsDir: "cases", + Mocks: m, + DB: db, + Aerospike: runner.Aerospike{ + Client: aerospikeClient, + Namespace: "test", + }, + // Π’ΠΈΠΏ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌΠΎΠΉ Π±Π°Π·Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ…, Π²ΠΎΠ·ΠΌΠΎΠΆΠ½Ρ‹Π΅ значСния fixtures.Postgres, fixtures.Mysql, fixtures.Aerospike, fixtures.CustomLoader + // Если Π² ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ DB Π½Π΅ пустой, Π° Π΄Π°Π½Π½Ρ‹ΠΉ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ Π½Π΅ Π½Π°Π·Π½Π°Ρ‡Π΅Π½, Π±ΡƒΠ΄Π΅Ρ‚ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒΡΡ Ρ‚ΠΈΠΏ Π±Π΄ fixtures.Postgresql + DbType: fixtures.Postgres, + FixturesDir: "fixtures", + }) +} +``` + +Начиная с вСрсии 1.18.3, Π΄ΠΎΠ±Π°Π²Π»Π΅Π½Π° ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° Π²Π½Π΅ΡˆΠ½ΠΈΡ… ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΉ для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ тСстовых Π΄Π°Π½Π½Ρ‹Ρ… ΠΈΠ· фикстур, Ссли gonkey ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ ΠΊΠ°ΠΊ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ°. +Π§Ρ‚ΠΎΠ±Ρ‹ Π½Π°Ρ‡Π°Ρ‚ΡŒ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ внСшний Π·Π°Π³Ρ€ΡƒΠ·Ρ‡ΠΈΠΊ, Π²Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΌΠΎΠ΄ΡƒΠ»ΡŒ, содСрТащий Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΡŽ интСрфСйса fixtures.Loader. + +ΠŸΡ€ΠΈΠΌΠ΅Ρ€ для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ Π΄Π°Π½Π½Ρ‹Ρ… Π² Redis + +```go +package test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/lamoda/gonkey/fixtures" + redisLoader "github.com/lamoda/gonkey/fixtures/redis" + // redisLoader "custom_module/gonkey-redis" // внСшняя Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ°, содСрТащая Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΡŽ интСрфСйса fixtures.Loader + redisClient "github.com/go-redis/redis/v9" + "github.com/lamoda/gonkey/runner" +) + +func TestFuncCases(t *testing.T) { + serveMux := http.NewServeMux() + + serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("ok")) + }) + + srv := httptest.NewServer(serveMux) + + redisFixtureLoader := redisLoader.New(redisLoader.LoaderOptions{ + FixtureDir: "./fixtures", + Redis: &redisClient.Options{ + Addr: "localhost:6379", + }, + }) + + runner.RunWithTesting(t, &runner.RunWithTestingParams{ + Server: srv, + TestsDir: "./cases", + DbType: fixtures.CustomLoader, + FixtureLoader: redisFixtureLoader, + }) } ``` @@ -607,7 +664,7 @@ tables: Для Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π° Aerospike Ρ‚Π°ΠΊΠΆΠ΅ поддСрТиваСтся Π·Π°Π»ΠΈΠ²ΠΊΠ° тСстовых Π΄Π°Π½Π½Ρ‹Ρ…. Для этого Π²Π°ΠΆΠ½ΠΎ Π½Π΅ Π·Π°Π±Ρ‹Ρ‚ΡŒ ΠΏΡ€ΠΈ запускС gonkey ΠΊΠ°ΠΊ CLI-ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Ρ„Π»Π°Π³ `-db-type aerospike`, Π° ΠΏΡ€ΠΈ использовании Π² качСствС Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ Π² ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ Ρ€Π°Π½Π½Π΅Ρ€Π°: `DbType: fixtures.Aerospike`. -Π€ΠΎΡ€ΠΌΠ°Ρ‚ Ρ„Π°ΠΉΠ»ΠΎΠ² с фикстурами для аэроспайка отличаСтся, Π½ΠΎ смысл остаётся ΠΏΡ€Π΅ΠΆΠ½ΠΈΠΌ: +Π€ΠΎΡ€ΠΌΠ°Ρ‚ Ρ„Π°ΠΉΠ»ΠΎΠ² с фикстурами для Aerospike отличаСтся, Π½ΠΎ смысл остаётся ΠΏΡ€Π΅ΠΆΠ½ΠΈΠΌ: ```yaml sets: set1: @@ -652,6 +709,147 @@ sets: БвязываниС записСй ΠΈ выраТСния Π½Π° Π΄Π°Π½Π½Ρ‹ΠΉ ΠΌΠΎΠΌΠ΅Π½Ρ‚ Π½Π΅ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°ΡŽΡ‚ΡΡ. +### Redis + +ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ΡΡ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ° тСстовых Π΄Π°Π½Π½Ρ‹Ρ… Ρ‡Π΅Ρ€Π΅Π· фикстуры для Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π° ΠΊΠ»ΡŽΡ‡/Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ Redis + +Бписок, ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅ΠΌΡ‹Ρ… структур Π΄Π°Π½Π½Ρ‹Ρ…: + +- ΠŸΠ°Ρ€Π° ΠΊΠ»ΡŽΡ‡/Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ +- Set +- Hash +- List +- ZSet (sorted set) + +БвязываниС записСй ΠΈ выраТСния Π½Π° Π΄Π°Π½Π½Ρ‹ΠΉ ΠΌΠΎΠΌΠ΅Π½Ρ‚ Π½Π΅ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°ΡŽΡ‚ΡΡ. + +ΠŸΡ€ΠΈΠΌΠ΅Ρ€ Ρ„Π°ΠΉΠ»Π° фикстуры: + +```yaml +inherits: + - template1 + - template2 + - other_fixture +templates: + keys: + - $name: parentKeyTemplate + values: + baseKey: + expiration: 1s + value: 1 + - $name: childKeyTemplate + $extend: parentKeyTemplate + values: + otherKey: + value: 2 + sets: + - $name: parentSetTemplate + expiration: 10s + values: + - value: a + - $name: childSetTemplate + $extend: parentSetTemplate + values: + - value: b + hashes: + - $name: parentHashTemplate + values: + - key: a + value: 1 + - key: b + value: 2 + - $name: childHashTemplate + $extend: parentHashTemplate + values: + - key: c + value: 3 + - key: d + value: 4 + lists: + - $name: parentListTemplate + values: + - value: 1 + - value: 2 + - $name: childListTemplate + values: + - value: 3 + - value: 4 + zsets: + - $name: parentZSetTemplate + values: + - value: 1 + score: 2.1 + - value: 2 + score: 4.3 + - $name: childZSetTemplate + value: + - value: 3 + score: 6.5 + - value: 4 + score: 8.7 +databases: + 1: + keys: + $extend: childKeyTemplate + values: + key1: + value: value1 + key2: + expiration: 10s + value: value2 + sets: + values: + set1: + $extend: childSetTemplate + expiration: 10s + values: + - value: a + - value: b + set3: + expiration: 5s + values: + - value: x + - value: y + hashes: + values: + map1: + $extend: childHashTemplate + values: + - key: a + value: 1 + - key: b + value: 2 + map2: + values: + - key: c + value: 3 + - key: d + value: 4 + lists: + values: + list1: + $extend: childListTemplate + values: + - value: 1 + - value: 100 + - value: 200 + zsets: + values: + zset1: + $extend: childZSetTemplate + values: + - value: 5 + score: 10.1 + 2: + keys: + values: + key3: + value: value3 + key4: + expiration: 5s + value: value4 +``` + ## Моки Π§Ρ‚ΠΎΠ±Ρ‹ для тСстов ΠΈΠΌΠΈΡ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ ΠΎΡ‚ Π²Π½Π΅ΡˆΠ½ΠΈΡ… сСрвисов, ΠΏΡ€ΠΈΠΌΠ΅Π½ΡΡŽΡ‚ΡΡ ΠΌΠΎΠΊΠΈ. diff --git a/README.md b/README.md index c82e311..4445627 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Capabilities: - works with REST/JSON API - tests service API for compliance with OpenAPI-specs -- seeds the DB with fixtures data (supports PostgreSQL, MySQL, Aerospike) +- seeds the DB with fixtures data (supports PostgreSQL, MySQL, Aerospike, Redis) - provides mocks for external services - can be used as a library and ran together with unit-tests - stores the results as an [Allure](http://allure.qatools.ru/) report @@ -35,6 +35,7 @@ Capabilities: - [Record linking](#record-linking) - [Expressions](#expressions) - [Aerospike](#aerospike) + - [Redis](#redis) - [Mocks](#mocks) - [Running mocks while using gonkey as a library](#running-mocks-while-using-gonkey-as-a-library) - [Mocks definition in the test file](#mocks-definition-in-the-test-file) @@ -60,8 +61,9 @@ To test a service located on a remote host, use gonkey as a console util. - `-spec <...>` path to a file or URL with the swagger-specs for the service - `-host <...>` service host:port - `-tests <...>` test file or directory -- `-db-type <...>` - database type. PostgreSQL and Aerospike are currently supported. -- `-aerospike_host <...>` when using Aerospike - connection URL in form of `host:port/namespace` +- `-db-type <...>` - database type. PostgreSQL, Aerospike, Redis are currently supported. +- `-aerospike_host <...>` when using Aerospike - connection URL in a form of `host:port/namespace` +- `-redis_addr <...>` when using Redis - connection address in a form of `host:port` - `-db_dsn <...>` DSN for the test DB (the DB will be cleared before seeding!), supports only PostgreSQL - `-fixtures <...>` fixtures directory - `-allure` generate an Allure-report @@ -88,35 +90,90 @@ import ( Create a test function. ```go +package test + +import ( + "testing" + + "github.com/lamoda/gonkey/fixtures" + "github.com/lamoda/gonkey/mocks" + "github.com/lamoda/gonkey/runner" +) + func TestFuncCases(t *testing.T) { - // init the mocks if needed (details below) - //m := mocks.NewNop(...) - - // init the DB to load the fixtures if needed (details below) - //db := ... - - // init Aerospike to load the fixtures if needed (details below) - //aerospikeClient := ... - - // create a server instance of your app - srv := server.NewServer() - defer srv.Close() - - // run test cases from your dir with Allure report generation - runner.RunWithTesting(t, &runner.RunWithTestingParams{ - Server: srv, - TestsDir: "cases", - Mocks: m, - DB: db, - Aerospike: runner.Aerospike{ - Client: aerospikeClient, - Namespace: "test", - } - // Type of database, can be fixtures.Postgres or fixtures.Mysql - // if DB parameter present, by default uses fixtures.Postgres database type - DbType: fixtures.Postgres, - FixturesDir: "fixtures", - }) + // init the mocks if needed (details below) + // m := mocks.NewNop(...) + + // init the DB to load the fixtures if needed (details below) + // db := ... + + // init Aerospike to load the fixtures if needed (details below) + // aerospikeClient := ... + + // create a server instance of your app + srv := server.NewServer() + defer srv.Close() + + // run test cases from your dir with Allure report generation + runner.RunWithTesting(t, &runner.RunWithTestingParams{ + Server: srv, + TestsDir: "cases", + Mocks: m, + DB: db, + Aerospike: runner.Aerospike{ + Client: aerospikeClient, + Namespace: "test", + }, + // Type of database, can be fixtures.Postgres, fixtures.Mysql, fixtures.CustomLoader + // if DB parameter present, by default uses fixtures.Postgres database type + DbType: fixtures.Postgres, + FixturesDir: "fixtures", + }) +} +``` + +Starts from version 1.18.3, externally written fixture loader may be used for loading test data, if gonkey used as a library. +To start using the custom loader, you need to import the custom module, that contains implementation of fixtures.Loader interface. + +Example with a redis fixtures loader: + +```go +package test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/lamoda/gonkey/fixtures" + redisLoader "github.com/lamoda/gonkey/fixtures/redis" + // redisLoader "custom_module/gonkey-redis" // custom implementation of a fixtures.Loader interface + redisClient "github.com/go-redis/redis/v9" + "github.com/lamoda/gonkey/runner" +) + +func TestFuncCases(t *testing.T) { + serveMux := http.NewServeMux() + + serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("ok")) + }) + + srv := httptest.NewServer(serveMux) + + redisFixtureLoader := redisLoader.New(redisLoader.LoaderOptions{ + FixtureDir: "./fixtures", + Redis: &redisClient.Options{ + Addr: "localhost:6379", + }, + }) + + runner.RunWithTesting(t, &runner.RunWithTestingParams{ + Server: srv, + TestsDir: "./cases", + DbType: fixtures.CustomLoader, + FixtureLoader: redisFixtureLoader, + }) } ``` @@ -656,6 +713,146 @@ sets: Records linking and expressions are currently not supported. +### Redis + +Supports loading test data with fixtures for redis key/value storage. +While using gonkey as a CLI application do not forget the flag `-db-type redis`. + +List of supported data structures: + + - Plain key/value + - Set + - Hash + - List + - ZSet (sorted set) + +Fixture file example: + +```yaml +inherits: + - template1 + - template2 + - other_fixture +templates: + keys: + - $name: parentKeyTemplate + values: + baseKey: + expiration: 1s + value: 1 + - $name: childKeyTemplate + $extend: parentKeyTemplate + values: + otherKey: + value: 2 + sets: + - $name: parentSetTemplate + expiration: 10s + values: + - value: a + - $name: childSetTemplate + $extend: parentSetTemplate + values: + - value: b + hashes: + - $name: parentHashTemplate + values: + - key: a + value: 1 + - key: b + value: 2 + - $name: childHashTemplate + $extend: parentHashTemplate + values: + - key: c + value: 3 + - key: d + value: 4 + lists: + - $name: parentListTemplate + values: + - value: 1 + - value: 2 + - $name: childListTemplate + values: + - value: 3 + - value: 4 + zsets: + - $name: parentZSetTemplate + values: + - value: 1 + score: 2.1 + - value: 2 + score: 4.3 + - $name: childZSetTemplate + value: + - value: 3 + score: 6.5 + - value: 4 + score: 8.7 +databases: + 1: + keys: + $extend: childKeyTemplate + values: + key1: + value: value1 + key2: + expiration: 10s + value: value2 + sets: + values: + set1: + $extend: childSetTemplate + expiration: 10s + values: + - value: a + - value: b + set3: + expiration: 5s + values: + - value: x + - value: y + hashes: + values: + map1: + $extend: childHashTemplate + values: + - key: a + value: 1 + - key: b + value: 2 + map2: + values: + - key: c + value: 3 + - key: d + value: 4 + lists: + values: + list1: + $extend: childListTemplate + values: + - value: 1 + - value: 100 + - value: 200 + zsets: + values: + zset1: + $extend: childZSetTemplate + values: + - value: 5 + score: 10.1 + 2: + keys: + values: + key3: + value: value3 + key4: + expiration: 5s + value: value4 +``` + ## Mocks In order to imitate responses from external services, use mocks. diff --git a/fixtures/aerospike/aerospike.go b/fixtures/aerospike/aerospike.go index e9439b0..ed34eb2 100644 --- a/fixtures/aerospike/aerospike.go +++ b/fixtures/aerospike/aerospike.go @@ -232,7 +232,7 @@ func (l *LoaderAerospike) truncateSet(name string) error { } func (l *LoaderAerospike) loadSet(ctx *loadContext, set loadedSet) error { - // $extend keyword allows to import values from a named row + // $extend keyword allows, to import values from a named row for key, binMap := range set.data { if base, ok := binMap["$extend"]; ok { baseName := base.(string) @@ -258,7 +258,7 @@ func (l *LoaderAerospike) loadSet(ctx *loadContext, set loadedSet) error { } // resolveReference finds previously stored reference by its name -func (f *LoaderAerospike) resolveReference(refs set, refName string) (binMap, error) { +func (l *LoaderAerospike) resolveReference(refs set, refName string) (binMap, error) { target, ok := refs[refName] if !ok { return nil, fmt.Errorf("undefined reference %s", refName) diff --git a/fixtures/loader.go b/fixtures/loader.go index d164cb7..24b56ba 100644 --- a/fixtures/loader.go +++ b/fixtures/loader.go @@ -18,20 +18,24 @@ const ( Postgres DbType = iota Mysql Aerospike + Redis + CustomLoader // using external loader if gonkey used as a library ) const ( PostgresParam = "postgres" MysqlParam = "mysql" AerospikeParam = "aerospike" + RedisParam = "redis" ) type Config struct { - DB *sql.DB - Aerospike *aerospikeClient.Client - DbType DbType - Location string - Debug bool + DB *sql.DB + Aerospike *aerospikeClient.Client + DbType DbType + Location string + Debug bool + FixtureLoader Loader } type Loader interface { @@ -64,6 +68,9 @@ func NewLoader(cfg *Config) Loader { cfg.Debug, ) default: + if cfg.FixtureLoader != nil { + return cfg.FixtureLoader + } panic("unknown db type") } @@ -78,6 +85,8 @@ func FetchDbType(dbType string) DbType { return Mysql case AerospikeParam: return Aerospike + case RedisParam: + return Redis default: panic("unknown db type param") } diff --git a/fixtures/redis/parser/context.go b/fixtures/redis/parser/context.go new file mode 100644 index 0000000..464e07c --- /dev/null +++ b/fixtures/redis/parser/context.go @@ -0,0 +1,19 @@ +package parser + +type context struct { + keyRefs map[string]Keys + hashRefs map[string]HashRecordValue + setRefs map[string]SetRecordValue + listRefs map[string]ListRecordValue + zsetRefs map[string]ZSetRecordValue +} + +func NewContext() *context { + return &context{ + keyRefs: make(map[string]Keys), + hashRefs: make(map[string]HashRecordValue), + setRefs: make(map[string]SetRecordValue), + listRefs: make(map[string]ListRecordValue), + zsetRefs: make(map[string]ZSetRecordValue), + } +} diff --git a/fixtures/redis/parser/file.go b/fixtures/redis/parser/file.go new file mode 100644 index 0000000..e662dcf --- /dev/null +++ b/fixtures/redis/parser/file.go @@ -0,0 +1,88 @@ +package parser + +import ( + "errors" + "fmt" + "path/filepath" + "strings" +) + +var ( + ErrFixtureNotFound = errors.New("fixture not found") + ErrFixtureFileLoad = errors.New("failed to load fixture file") + ErrFixtureParseFile = errors.New("failed to parse fixture file") + ErrParserNotFound = errors.New("parser not found") +) + +func loadError(fixtureName string, err error) error { + return fmt.Errorf("%w %s: %s", ErrFixtureFileLoad, fixtureName, err) +} + +func parseError(fixtureName string, err error) error { + return fmt.Errorf("%w %s: %s", ErrFixtureParseFile, fixtureName, err) +} + +type fileParser struct { + locations []string +} + +func New(locations []string) *fileParser{ + return &fileParser{ + locations: locations, + } +} + +func (l *fileParser) ParseFiles(ctx *context, names []string) ([]*Fixture, error) { + var fileNameCache = make(map[string]struct{}) + var fixtures []*Fixture + + for _, name := range names { + for _, loc := range l.locations { + filename, err := l.getFirstExistsFileName(name, loc) + if err != nil { + return nil, loadError(name, err) + } + if _, ok := fileNameCache[filename]; ok { + continue + } + + extension := strings.Replace(filepath.Ext(filename), ".", "", -1) + fixtureParser := GetParser(extension) + if fixtureParser == nil { + return nil, ErrParserNotFound + } + parserCopy := fixtureParser.Copy(l) + + fixture, err := parserCopy.Parse(ctx, filename) + if err != nil { + return nil, parseError(filename, err) + } + + fixtures = append(fixtures, fixture) + fileNameCache[filename] = struct{}{} + } + } + + return fixtures, nil +} + +func (l *fileParser) getFirstExistsFileName(name string, location string) (string, error) { + candidates := []string{ + name, + fmt.Sprintf("%s.yaml", name), + fmt.Sprintf("%s.yml", name), + } + + for _, p := range candidates { + path := filepath.Join(location, p) + paths, err := filepath.Glob(path) + if err != nil { + return "", err + } + if len(paths) > 0 { + return paths[0], nil + } + } + + return "", ErrFixtureNotFound +} diff --git a/fixtures/redis/parser/fixture.go b/fixtures/redis/parser/fixture.go new file mode 100644 index 0000000..31cb8d4 --- /dev/null +++ b/fixtures/redis/parser/fixture.go @@ -0,0 +1,188 @@ +package parser + +import ( + "fmt" + "time" +) + +// Fixture is a representation of the test data, that is preloaded to a redis database before the test starts +/* Example (yaml): +```yaml +inherits: + - parent_template + - child_template + - other_fixture +databases: + 1: + keys: + $name: keys1 + values: + a: + value: 1 + expiration: 10s + b: + value: 2 + 2: + keys: + $name: keys2 + values: + c: + value: 3 + expiration: 10s + d: + value: 4 +``` +*/ +type Fixture struct { + Inherits []string `yaml:"inherits"` + Templates Templates `yaml:"templates"` + Databases map[int]Database `yaml:"databases"` +} + +type Templates struct { + Keys []*Keys `yaml:"keys"` + Hashes []*HashRecordValue `yaml:"hashes"` + Sets []*SetRecordValue `yaml:"sets"` + Lists []*ListRecordValue `yaml:"lists"` + ZSets []*ZSetRecordValue `yaml:"zsets"` +} + +// Database contains data to load into Redis database +type Database struct { + Keys *Keys `yaml:"keys"` + Hashes *Hashes `yaml:"hashes"` + Sets *Sets `yaml:"sets"` + Lists *Lists `yaml:"lists"` + ZSets *ZSets `yaml:"zsets"` +} + +const ( + TypeInt = "int" + TypeStr = "str" + TypeFloat = "float" +) + +type Value struct { + Type string + Value interface{} +} + +func (obj *Value) UnmarshalYAML(unmarshal func(interface{}) error) error { + var internal interface{} + if err := unmarshal(&internal); err != nil { + return err + } + switch v := internal.(type) { + case string: + obj.Type = TypeStr + obj.Value = v + case int, int16, int32, int64: + obj.Type = TypeInt + obj.Value = v + case float64: + obj.Type = TypeFloat + obj.Value = v + default: + return fmt.Errorf("unknown value type: %T", v) + } + + return nil +} + +func Str(value string) Value { + return Value{Type: TypeStr, Value: value} +} + +func Int(value int) Value { + return Value{Type: TypeInt, Value: value} +} + +func Float(value float64) Value { + return Value{Type: TypeFloat, Value: value} +} + +// Keys represent a collection of key/value pairs, that will be loaded into Redis database +type Keys struct { + Name string `yaml:"$name"` + Extend string `yaml:"$extend"` + Values map[string]*KeyValue `yaml:"values"` +} + +// KeyValue represent a redis key/value pair +type KeyValue struct { + Value Value `yaml:"value"` + Expiration time.Duration `yaml:"expiration"` +} + +// Hashes represent a collection of hash data structures, that will be loaded into Redis database +type Hashes struct { + Values map[string]*HashRecordValue `yaml:"values"` +} + +// HashRecordValue represent a single hash data structure +type HashRecordValue struct { + Name string `yaml:"$name"` + Extend string `yaml:"$extend"` + Values []*HashValue `yaml:"values"` + Expiration time.Duration `yaml:"expiration"` +} + +type HashValue struct { + Key Value `yaml:"key"` + Value Value `yaml:"value"` +} + +// Sets represent a collection of set data structures, that will be loaded into Redis database +type Sets struct { + Values map[string]*SetRecordValue `yaml:"values"` +} + +// SetRecordValue represent a single set data structure +type SetRecordValue struct { + Name string `yaml:"$name"` + Extend string `yaml:"$extend"` + Values []*SetValue `yaml:"values"` + Expiration time.Duration `yaml:"expiration"` +} + +// SetValue represent a set value object +type SetValue struct { + Value Value `yaml:"value"` +} + +// Lists represent a collection of Redis list data structures +type Lists struct { + Values map[string]*ListRecordValue `yaml:"values"` +} + +// ListRecordValue represent a single list data structure +type ListRecordValue struct { + Name string `yaml:"$name"` + Extend string `yaml:"$extend"` + Values []*ListValue `yaml:"values"` + Expiration time.Duration `yaml:"expiration"` +} + +// ListValue represent a list value object +type ListValue struct { + Value Value `yaml:"value"` +} + +// ZSets represent a collection of Redis sorted set data structure +type ZSets struct { + Values map[string]*ZSetRecordValue `yaml:"values"` +} + +// ZSetRecordValue represent a single sorted set data structure +type ZSetRecordValue struct { + Name string `yaml:"$name"` + Extend string `yaml:"$extend"` + Values []*ZSetValue `yaml:"values"` + Expiration time.Duration `yaml:"expiration"` +} + +// ZSetValue represent a zset value object +type ZSetValue struct { + Value Value `yaml:"value"` + Score float64 `yaml:"score"` +} diff --git a/fixtures/redis/parser/parser.go b/fixtures/redis/parser/parser.go new file mode 100644 index 0000000..ddb3fe1 --- /dev/null +++ b/fixtures/redis/parser/parser.go @@ -0,0 +1,21 @@ +package parser + +type FixtureFileParser interface { + Parse(ctx *context, filename string) (*Fixture, error) + Copy(parser *fileParser) FixtureFileParser +} + +var fixtureParsersRegistry = make(map[string]FixtureFileParser) + +func RegisterParser(format string, parser FixtureFileParser) { + fixtureParsersRegistry[format] = parser +} + +func GetParser(format string) FixtureFileParser { + return fixtureParsersRegistry[format] +} + +func init() { + RegisterParser("yaml", &redisYamlParser{}) +} + diff --git a/fixtures/redis/parser/parser_test.go b/fixtures/redis/parser/parser_test.go new file mode 100644 index 0000000..d5299fc --- /dev/null +++ b/fixtures/redis/parser/parser_test.go @@ -0,0 +1,722 @@ +package parser + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestRedisFixtureParser_Load(t *testing.T) { + type args struct { + fixtures []string + } + + type want struct { + fixtures []*Fixture + ctx *context + } + + tests := []struct { + name string + args args + want want + }{ + { + name: "test basic", + args: args{ + fixtures: []string{"redis"}, + }, + want: want{ + fixtures: []*Fixture{ + { + Databases: map[int]Database{ + 1: { + Keys: &Keys{ + Values: map[string]*KeyValue{ + "key1": { + Value: Str("value1"), + }, + "key2": { + Value: Str("value2"), + Expiration: time.Second * 10, + }, + }, + }, + Sets: &Sets{ + Values: map[string]*SetRecordValue{ + "set1": { + Expiration: time.Second * 10, + Values: []*SetValue{ + {Value: Str("a")}, + {Value: Str("b")}, + {Value: Str("c")}, + }, + }, + "set3": { + Expiration: time.Second * 5, + Values: []*SetValue{ + {Value: Str("x")}, + {Value: Str("y")}, + }, + }, + }, + }, + Hashes: &Hashes{ + Values: map[string]*HashRecordValue{ + "map1": { + Values: []*HashValue{ + {Value: Int(1), Key: Str("a")}, + {Value: Int(2), Key: Str("b")}, + }, + }, + "map2": { + Values: []*HashValue{ + {Value: Int(3), Key: Str("c")}, + {Value: Int(4), Key: Str("d")}, + }, + }, + }, + }, + Lists: &Lists{ + Values: map[string]*ListRecordValue{ + "list1": { + Values: []*ListValue{ + {Value: Int(1)}, + {Value: Str("2")}, + {Value: Str("a")}, + }, + }, + }, + }, + ZSets: &ZSets{ + Values: map[string]*ZSetRecordValue{ + "zset1": { + Values: []*ZSetValue{ + {Value: Int(1), Score: 1.1}, + {Value: Str("2"), Score: 5.6}, + }, + }, + }, + }, + }, + 2: { + Keys: &Keys{ + Values: map[string]*KeyValue{ + "key3": { + Value: Str("value3"), + }, + "key4": { + Value: Str("value4"), + Expiration: time.Second * 5, + }, + }, + }, + Sets: &Sets{ + Values: map[string]*SetRecordValue{ + "set2": { + Expiration: time.Second * 5, + Values: []*SetValue{ + {Value: Str("d")}, + {Value: Str("e")}, + {Value: Str("f")}, + }, + }, + }, + }, + Hashes: &Hashes{ + Values: map[string]*HashRecordValue{ + "map3": { + Values: []*HashValue{ + {Value: Int(3), Key: Str("c")}, + {Value: Int(4), Key: Str("d")}, + }, + }, + "map4": { + Values: []*HashValue{ + {Value: Int(10), Key: Str("e")}, + {Value: Int(11), Key: Str("f")}, + }, + }, + }, + }, + }, + }, + }, + }, + ctx: &context{ + keyRefs: map[string]Keys{}, + setRefs: map[string]SetRecordValue{}, + hashRefs: map[string]HashRecordValue{}, + listRefs: map[string]ListRecordValue{}, + zsetRefs: map[string]ZSetRecordValue{}, + }, + }, + }, + { + name: "extend", + args: args{ + fixtures: []string{"redis_extend"}, + }, + want: want{ + fixtures: []*Fixture{ + { + Templates: Templates{ + Keys: []*Keys{ + { + Name: "parentKeys", + Values: map[string]*KeyValue{ + "a": {Value: Int(1)}, + "b": {Value: Int(2)}, + }, + }, + { + Name: "childKeys", + Extend: "parentKeys", + Values: map[string]*KeyValue{ + "a": {Value: Int(1)}, + "b": {Value: Int(2)}, + "c": {Value: Int(3)}, + "d": {Value: Int(4)}, + }, + }, + }, + Sets: []*SetRecordValue{ + { + Name: "parentSet", + Expiration: time.Second * 10, + Values: []*SetValue{ + {Value: Str("a")}, + {Value: Str("b")}, + }, + }, + { + Name: "childSet", + Extend: "parentSet", + Expiration: time.Second * 10, + Values: []*SetValue{ + {Value: Str("a")}, + {Value: Str("b")}, + {Value: Str("c")}, + }, + }, + }, + Hashes: []*HashRecordValue{ + { + Name: "parentMap", + Values: []*HashValue{ + {Value: Int(1), Key: Str("a1")}, + {Value: Int(2), Key: Str("b1")}, + }, + }, + { + Name: "childMap", + Extend: "parentMap", + Values: []*HashValue{ + {Value: Int(1), Key: Str("a1")}, + {Value: Int(2), Key: Str("b1")}, + {Value: Int(3), Key: Str("c1")}, + }, + }, + }, + Lists: []*ListRecordValue{ + { + Name: "parentList", + Values: []*ListValue{ + {Value: Int(1)}, + {Value: Int(2)}, + }, + }, + { + Name: "childList", + Extend: "parentList", + Values: []*ListValue{ + {Value: Int(1)}, + {Value: Int(2)}, + {Value: Int(3)}, + {Value: Int(4)}, + }, + }, + }, + ZSets: []*ZSetRecordValue{ + { + Name: "parentZSet", + Values: []*ZSetValue{ + {Value: Int(1), Score: 1.2}, + {Value: Int(2), Score: 3.4}, + }, + }, + { + Name: "childZSet", + Extend: "parentZSet", + Values: []*ZSetValue{ + {Value: Int(1), Score: 1.2}, + {Value: Int(2), Score: 3.4}, + {Value: Int(3), Score: 5.6}, + {Value: Int(4), Score: 7.8}, + }, + }, + }, + }, + Databases: map[int]Database{ + 1: { + Keys: &Keys{ + Extend: "childKeys", + Values: map[string]*KeyValue{ + "a": {Value: Int(1)}, + "b": {Value: Int(2)}, + "c": {Value: Int(3)}, + "d": {Value: Int(4)}, + "key1": { + Value: Str("value1"), + }, + "key2": { + Value: Str("value2"), + Expiration: time.Second * 10, + }, + }, + }, + Sets: &Sets{ + Values: map[string]*SetRecordValue{ + "set1": { + Extend: "childSet", + Expiration: time.Second * 10, + Values: []*SetValue{ + {Value: Str("a")}, + {Value: Str("b")}, + {Value: Str("c")}, + {Value: Str("d")}, + }, + }, + "set2": { + Expiration: time.Second * 10, + Values: []*SetValue{ + {Value: Str("x")}, + {Value: Str("y")}, + }, + }, + }, + }, + Hashes: &Hashes{ + Values: map[string]*HashRecordValue{ + "map1": { + Name: "baseMap", + Extend: "childMap", + Values: []*HashValue{ + {Value: Int(1), Key: Str("a1")}, + {Value: Int(2), Key: Str("b1")}, + {Value: Int(3), Key: Str("c1")}, + {Value: Int(1), Key: Str("a")}, + {Value: Int(2), Key: Str("b")}, + }, + }, + "map2": { + Values: []*HashValue{ + {Value: Int(3), Key: Str("c")}, + {Value: Int(4), Key: Str("d")}, + }, + }, + }, + }, + Lists: &Lists{ + Values: map[string]*ListRecordValue{ + "list1": { + Name: "list1", + Extend: "childList", + Values: []*ListValue{ + {Value: Int(1)}, + {Value: Int(2)}, + {Value: Int(3)}, + {Value: Int(4)}, + {Value: Int(10)}, + {Value: Int(11)}, + }, + }, + }, + }, + ZSets: &ZSets{ + Values: map[string]*ZSetRecordValue{ + "zset1": { + Name: "zset1", + Extend: "childZSet", + Values: []*ZSetValue{ + {Value: Int(1), Score: 1.2}, + {Value: Int(2), Score: 3.4}, + {Value: Int(3), Score: 5.6}, + {Value: Int(4), Score: 7.8}, + {Value: Int(5), Score: 10.1}, + }, + }, + }, + }, + }, + }, + }, + }, + ctx: &context{ + keyRefs: map[string]Keys{ + "parentKeys": { + Values: map[string]*KeyValue{ + "a": {Value: Int(1)}, + "b": {Value: Int(2)}, + }, + }, + "childKeys": { + Values: map[string]*KeyValue{ + "a": {Value: Int(1)}, + "b": {Value: Int(2)}, + "c": {Value: Int(3)}, + "d": {Value: Int(4)}, + }, + }, + }, + setRefs: map[string]SetRecordValue{ + "parentSet": { + Expiration: time.Second * 10, + Values: []*SetValue{ + {Value: Str("a")}, + {Value: Str("b")}, + }, + }, + "childSet": { + Expiration: time.Second * 10, + Values: []*SetValue{ + {Value: Str("a")}, + {Value: Str("b")}, + {Value: Str("c")}, + }, + }, + }, + hashRefs: map[string]HashRecordValue{ + "baseMap": { + Values: []*HashValue{ + {Value: Int(1), Key: Str("a1")}, + {Value: Int(2), Key: Str("b1")}, + {Value: Int(3), Key: Str("c1")}, + {Value: Int(1), Key: Str("a")}, + {Value: Int(2), Key: Str("b")}, + }, + }, + "parentMap": { + Values: []*HashValue{ + {Value: Int(1), Key: Str("a1")}, + {Value: Int(2), Key: Str("b1")}, + }, + }, + "childMap": { + Values: []*HashValue{ + {Value: Int(1), Key: Str("a1")}, + {Value: Int(2), Key: Str("b1")}, + {Value: Int(3), Key: Str("c1")}, + }, + }, + }, + listRefs: map[string]ListRecordValue{ + "parentList": { + Values: []*ListValue{ + {Value: Int(1)}, + {Value: Int(2)}, + }, + }, + "childList": { + Values: []*ListValue{ + {Value: Int(1)}, + {Value: Int(2)}, + {Value: Int(3)}, + {Value: Int(4)}, + }, + }, + "list1": { + Values: []*ListValue{ + {Value: Int(1)}, + {Value: Int(2)}, + {Value: Int(3)}, + {Value: Int(4)}, + {Value: Int(10)}, + {Value: Int(11)}, + }, + }, + }, + zsetRefs: map[string]ZSetRecordValue{ + "parentZSet": { + Values: []*ZSetValue{ + {Value: Int(1), Score: 1.2}, + {Value: Int(2), Score: 3.4}, + }, + }, + "childZSet": { + Values: []*ZSetValue{ + {Value: Int(1), Score: 1.2}, + {Value: Int(2), Score: 3.4}, + {Value: Int(3), Score: 5.6}, + {Value: Int(4), Score: 7.8}, + }, + }, + "zset1": { + Values: []*ZSetValue{ + {Value: Int(1), Score: 1.2}, + {Value: Int(2), Score: 3.4}, + {Value: Int(3), Score: 5.6}, + {Value: Int(4), Score: 7.8}, + {Value: Int(5), Score: 10.1}, + }, + }, + }, + }, + }, + }, + { + name: "inherits", + args: args{ + fixtures: []string{"redis_inherits"}, + }, + want: want{ + fixtures: []*Fixture{ + { + Inherits: []string{"redis_extend"}, + Databases: map[int]Database{ + 1: { + Keys: &Keys{ + Extend: "childKeys", + Values: map[string]*KeyValue{ + "a": {Value: Int(1)}, + "b": {Value: Int(2)}, + "c": {Value: Int(3)}, + "d": {Value: Int(4)}, + "key1": {Value: Str("value1")}, + "key2": {Value: Str("value2"), Expiration: time.Second * 10}, + }, + }, + Sets: &Sets{ + Values: map[string]*SetRecordValue{ + "set1": { + Extend: "childSet", + Expiration: time.Second * 10, + Values: []*SetValue{ + {Value: Str("a")}, + {Value: Str("b")}, + {Value: Str("c")}, + }, + }, + }, + }, + Hashes: &Hashes{ + Values: map[string]*HashRecordValue{ + "map1": { + Extend: "baseMap", + Values: []*HashValue{ + {Value: Int(1), Key: Str("a1")}, + {Value: Int(2), Key: Str("b1")}, + {Value: Int(3), Key: Str("c1")}, + {Value: Int(1), Key: Str("a")}, + {Value: Int(2), Key: Str("b")}, + {Value: Int(10), Key: Str("x")}, + {Value: Int(11), Key: Str("y")}, + }, + }, + "map2": { + Extend: "childMap", + Values: []*HashValue{ + {Value: Int(1), Key: Str("a1")}, + {Value: Int(2), Key: Str("b1")}, + {Value: Int(3), Key: Str("c1")}, + {Value: Int(500), Key: Str("t")}, + {Value: Int(1000), Key: Str("j")}, + }, + }, + }, + }, + Lists: &Lists{ + Values: map[string]*ListRecordValue{ + "list2": { + Extend: "list1", + Values: []*ListValue{ + {Value: Int(1)}, + {Value: Int(2)}, + {Value: Int(3)}, + {Value: Int(4)}, + {Value: Int(10)}, + {Value: Int(11)}, + {Value: Int(100)}, + }, + }, + "list3": { + Extend: "childList", + Values: []*ListValue{ + {Value: Int(1)}, + {Value: Int(2)}, + {Value: Int(3)}, + {Value: Int(4)}, + {Value: Int(200)}, + }, + }, + }, + }, + ZSets: &ZSets{ + Values: map[string]*ZSetRecordValue{ + "zset2": { + Extend: "zset1", + Values: []*ZSetValue{ + {Value: Int(1), Score: 1.2}, + {Value: Int(2), Score: 3.4}, + {Value: Int(3), Score: 5.6}, + {Value: Int(4), Score: 7.8}, + {Value: Int(5), Score: 10.1}, + {Value: Int(100), Score: 100.1}, + }, + }, + "zset3": { + Extend: "childZSet", + Values: []*ZSetValue{ + {Value: Int(1), Score: 1.2}, + {Value: Int(2), Score: 3.4}, + {Value: Int(3), Score: 5.6}, + {Value: Int(4), Score: 7.8}, + {Value: Int(200), Score: 200.2}, + }, + }, + }, + }, + }, + }, + }, + }, + ctx: &context{ + keyRefs: map[string]Keys{ + "parentKeys": { + Values: map[string]*KeyValue{ + "a": {Value: Int(1)}, + "b": {Value: Int(2)}, + }, + }, + "childKeys": { + Values: map[string]*KeyValue{ + "a": {Value: Int(1)}, + "b": {Value: Int(2)}, + "c": {Value: Int(3)}, + "d": {Value: Int(4)}, + }, + }, + }, + setRefs: map[string]SetRecordValue{ + "parentSet": { + Expiration: time.Second * 10, + Values: []*SetValue{ + {Value: Str("a")}, + {Value: Str("b")}, + }, + }, + "childSet": { + Expiration: time.Second * 10, + Values: []*SetValue{ + {Value: Str("a")}, + {Value: Str("b")}, + {Value: Str("c")}, + }, + }, + }, + hashRefs: map[string]HashRecordValue{ + "baseMap": { + Values: []*HashValue{ + {Value: Int(1), Key: Str("a1")}, + {Value: Int(2), Key: Str("b1")}, + {Value: Int(3), Key: Str("c1")}, + {Value: Int(1), Key: Str("a")}, + {Value: Int(2), Key: Str("b")}, + }, + }, + "parentMap": { + Values: []*HashValue{ + {Value: Int(1), Key: Str("a1")}, + {Value: Int(2), Key: Str("b1")}, + }, + }, + "childMap": { + Values: []*HashValue{ + {Value: Int(1), Key: Str("a1")}, + {Value: Int(2), Key: Str("b1")}, + {Value: Int(3), Key: Str("c1")}, + }, + }, + }, + listRefs: map[string]ListRecordValue{ + "parentList": { + Values: []*ListValue{ + {Value: Int(1)}, + {Value: Int(2)}, + }, + }, + "childList": { + Values: []*ListValue{ + {Value: Int(1)}, + {Value: Int(2)}, + {Value: Int(3)}, + {Value: Int(4)}, + }, + }, + "list1": { + Values: []*ListValue{ + {Value: Int(1)}, + {Value: Int(2)}, + {Value: Int(3)}, + {Value: Int(4)}, + {Value: Int(10)}, + {Value: Int(11)}, + }, + }, + }, + zsetRefs: map[string]ZSetRecordValue{ + "parentZSet": { + Values: []*ZSetValue{ + {Value: Int(1), Score: 1.2}, + {Value: Int(2), Score: 3.4}, + }, + }, + "childZSet": { + Values: []*ZSetValue{ + {Value: Int(1), Score: 1.2}, + {Value: Int(2), Score: 3.4}, + {Value: Int(3), Score: 5.6}, + {Value: Int(4), Score: 7.8}, + }, + }, + "zset1": { + Values: []*ZSetValue{ + {Value: Int(1), Score: 1.2}, + {Value: Int(2), Score: 3.4}, + {Value: Int(3), Score: 5.6}, + {Value: Int(4), Score: 7.8}, + {Value: Int(5), Score: 10.1}, + }, + }, + }, + }, + }, + }, + } + + p := New([]string{"../../testdata"}) + + // test parsing example file from README + _, err := p.ParseFiles(NewContext(), []string{"redis_example"}) + if err != nil { + t.Errorf("example file test error: %s", err) + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := NewContext() + fixtures, err := p.ParseFiles(ctx, test.args.fixtures) + if err != nil { + t.Errorf("ParseFiles - unexpected error: %s", err) + return + } + if diff := cmp.Diff(test.want.fixtures, fixtures); diff != "" { + t.Errorf("ParseFiles - unexpected diff in fixtures: %s", diff) + } + if diff := cmp.Diff(test.want.ctx, ctx, cmp.AllowUnexported(context{})); diff != "" { + t.Errorf("ParseFiles - unexpected diff in context: %s", diff) + } + }) + } +} diff --git a/fixtures/redis/parser/yaml.go b/fixtures/redis/parser/yaml.go new file mode 100644 index 0000000..96a199e --- /dev/null +++ b/fixtures/redis/parser/yaml.go @@ -0,0 +1,478 @@ +package parser + +import ( + "errors" + "fmt" + "io/ioutil" + + "gopkg.in/yaml.v3" +) + +type redisYamlParser struct { + fileParser *fileParser +} + +func (p *redisYamlParser) Copy(fileParser *fileParser) FixtureFileParser { + cp := &(*p) + cp.fileParser = fileParser + return cp +} + +func (p *redisYamlParser) extendKeys(ctx *context, child *Keys) error { + if child.Extend == "" { + return nil + } + parent, err := p.resolveKeyReference(ctx.keyRefs, child.Extend) + if err != nil { + return err + } + for k, v := range child.Values { + parent.Values[k] = v + } + child.Values = parent.Values + return nil +} + +func (p *redisYamlParser) copyKeyRecord(src *Keys) Keys { + keyRef := Keys{ + Values: make(map[string]*KeyValue, len(src.Values)), + } + for k, v := range src.Values { + var valueCopy *KeyValue + if v != nil { + valueCopy = &(*v) + } + keyRef.Values[k] = valueCopy + } + return keyRef +} + +func (p *redisYamlParser) extendSet(ctx *context, child *SetRecordValue) error { + if child.Extend == "" { + return nil + } + parent, err := p.resolveSetReference(ctx.setRefs, child.Extend) + if err != nil { + return err + } + var keys []interface{} + parentValuesMapped := make(map[interface{}]*SetValue) + for _, v := range parent.Values { + parentValuesMapped[v] = v + keys = append(keys, v) + } + for _, v := range child.Values { + if _, ok := parentValuesMapped[v]; !ok { + keys = append(keys, v) + } + parentValuesMapped[v] = v + } + setValues := make([]*SetValue, 0, len(parentValuesMapped)) + for _, k := range keys { + setValues = append(setValues, parentValuesMapped[k]) + } + child.Expiration = parent.Expiration + child.Values = setValues + return nil +} + +func (p *redisYamlParser) copySetRecord(src *SetRecordValue) SetRecordValue{ + setRef := SetRecordValue{ + Expiration: src.Expiration, + Values: make([]*SetValue, 0, len(src.Values)), + } + for _, v := range src.Values { + var valueCopy *SetValue + if v != nil { + valueCopy = &(*v) + } + setRef.Values = append(setRef.Values, valueCopy) + } + return setRef +} + +func (p *redisYamlParser) extendHash(ctx *context, child *HashRecordValue) error { + if child.Extend == "" { + return nil + } + parent, err := p.resolveHashReference(ctx.hashRefs, child.Extend) + if err != nil { + return err + } + var keys []interface{} + parentValuesMapped := make(map[interface{}]*HashValue) + for _, v := range parent.Values { + parentValuesMapped[v.Key] = v + keys = append(keys, v.Key) + } + for _, v := range child.Values { + if _, ok := parentValuesMapped[v.Key]; !ok { + keys = append(keys, v.Key) + } + parentValuesMapped[v.Key] = v + } + hashValues := make([]*HashValue, 0, len(parentValuesMapped)) + for _, k := range keys { + hashValues = append(hashValues, parentValuesMapped[k]) + } + + child.Expiration = parent.Expiration + child.Values = hashValues + return nil +} + +func (p *redisYamlParser) copyHashRecord(src *HashRecordValue) HashRecordValue { + cpy := HashRecordValue{ + Expiration: src.Expiration, + Values: make([]*HashValue, 0, len(src.Values)), + } + for _, v := range src.Values { + var valueCopy *HashValue + if v != nil { + valueCopy = &(*v) + } + cpy.Values = append(cpy.Values, valueCopy) + } + return cpy +} + +func (p *redisYamlParser) extendList(ctx *context, child *ListRecordValue) error { + if child.Extend == "" { + return nil + } + parent, err := p.resolveListReference(ctx.listRefs, child.Extend) + if err != nil { + return err + } + for _, v := range child.Values { + parent.Values = append(parent.Values, v) + } + child.Expiration = parent.Expiration + child.Values = parent.Values + return nil +} + +func (p *redisYamlParser) copyListRecord(src *ListRecordValue) ListRecordValue { + ref := ListRecordValue{ + Expiration: src.Expiration, + Values: make([]*ListValue, 0, len(src.Values)), + } + for _, v := range src.Values { + var valueCopy *ListValue + if v != nil { + valueCopy = &(*v) + } + ref.Values = append(ref.Values, valueCopy) + } + return ref +} + +func (p *redisYamlParser) extendZSet(ctx *context, child *ZSetRecordValue) error { + if child.Extend == "" { + return nil + } + parent, err := p.resolveZSetReference(ctx.zsetRefs, child.Extend) + if err != nil { + return err + } + var keys []interface{} + parentValuesMapped := make(map[interface{}]*ZSetValue) + for _, v := range parent.Values { + parentValuesMapped[v] = v + keys = append(keys, v) + } + for _, v := range child.Values { + if _, ok := parentValuesMapped[v]; !ok { + keys = append(keys, v) + } + parentValuesMapped[v] = v + } + setValues := make([]*ZSetValue, 0, len(parentValuesMapped)) + for _, k := range keys { + setValues = append(setValues, parentValuesMapped[k]) + } + + child.Expiration = parent.Expiration + child.Values = setValues + return nil +} + +func (p *redisYamlParser) copyZSetRecord(src *ZSetRecordValue) ZSetRecordValue { + ref := ZSetRecordValue{ + Expiration: src.Expiration, + Values: make([]*ZSetValue, 0, len(src.Values)), + } + for _, v := range src.Values { + var valueCopy *ZSetValue + if v != nil { + valueCopy = &(*v) + } + ref.Values = append(ref.Values, valueCopy) + } + return ref +} + +func (p *redisYamlParser) buildKeysTemplates(ctx *context, f Fixture) error { + for _, tplData := range f.Templates.Keys { + refName := tplData.Name + if refName == "" { + return errors.New("template $name is required") + } + if _, ok := ctx.keyRefs[refName]; ok { + return fmt.Errorf("unable to load template %s: duplicating ref name", refName) + } + if err := p.extendKeys(ctx, tplData); err != nil { + return err + } + ctx.keyRefs[refName] = p.copyKeyRecord(tplData) + } + return nil +} + +func (p *redisYamlParser) buildSetTemplates(ctx *context, f Fixture) error { + for _, tplData := range f.Templates.Sets { + refName := tplData.Name + if refName == "" { + return errors.New("template $name is required") + } + if _, ok := ctx.setRefs[refName]; ok { + return fmt.Errorf("unable to load template %s: duplicating ref name", refName) + } + if err := p.extendSet(ctx, tplData); err != nil { + return err + } + ctx.setRefs[refName] = p.copySetRecord(tplData) + } + return nil +} + +func (p *redisYamlParser) buildHashTemplates(ctx *context, f Fixture) error { + for _, tplData := range f.Templates.Hashes { + refName := tplData.Name + if refName == "" { + return errors.New("template $name is required") + } + if _, ok := ctx.hashRefs[refName]; ok { + return fmt.Errorf("unable to load template %s: duplicating ref name", refName) + } + if err := p.extendHash(ctx, tplData); err != nil { + return err + } + ctx.hashRefs[refName] = p.copyHashRecord(tplData) + } + return nil +} + +func (p *redisYamlParser) buildListTemplates(ctx *context, f Fixture) error { + for _, tplData := range f.Templates.Lists { + refName := tplData.Name + if refName == "" { + return errors.New("template $name is required") + } + if _, ok := ctx.listRefs[refName]; ok { + return fmt.Errorf("unable to load template %s: duplicating ref name", refName) + } + if err := p.extendList(ctx, tplData); err != nil { + return err + } + ctx.listRefs[refName] = p.copyListRecord(tplData) + } + return nil +} + +func (p *redisYamlParser) buildZSetTemplates(ctx *context, f Fixture) error { + for _, tplData := range f.Templates.ZSets { + refName := tplData.Name + if refName == "" { + return errors.New("template $name is required") + } + if _, ok := ctx.zsetRefs[refName]; ok { + return fmt.Errorf("unable to load template %s: duplicating ref name", refName) + } + if err := p.extendZSet(ctx, tplData); err != nil { + return err + } + ctx.zsetRefs[refName] = p.copyZSetRecord(tplData) + } + return nil +} + +func (p *redisYamlParser) buildTemplate(ctx *context, f Fixture) error { + if err := p.buildKeysTemplates(ctx, f); err != nil { + return err + } + if err := p.buildSetTemplates(ctx, f); err != nil { + return err + } + if err := p.buildHashTemplates(ctx, f); err != nil { + return err + } + if err := p.buildListTemplates(ctx, f); err != nil { + return err + } + if err := p.buildZSetTemplates(ctx, f); err != nil { + return err + } + return nil +} + +func (p *redisYamlParser) resolveKeyReference(refs map[string]Keys, refName string) (*Keys, error) { + refTemplate, ok := refs[refName] + if !ok { + return nil, fmt.Errorf("ref not found: %s", refName) + } + cpy := p.copyKeyRecord(&refTemplate) + return &cpy, nil +} + +func (p *redisYamlParser) resolveSetReference(refs map[string]SetRecordValue, refName string) (*SetRecordValue, error) { + refTemplate, ok := refs[refName] + if !ok { + return nil, fmt.Errorf("ref not found: %s", refName) + } + cpy := p.copySetRecord(&refTemplate) + return &cpy, nil +} + +func (p *redisYamlParser) resolveHashReference(refs map[string]HashRecordValue, refName string) (*HashRecordValue, error) { + refTemplate, ok := refs[refName] + if !ok { + return nil, fmt.Errorf("ref not found: %s", refName) + } + cpy := p.copyHashRecord(&refTemplate) + return &cpy, nil +} + +func (p *redisYamlParser) resolveListReference(refs map[string]ListRecordValue, refName string) (*ListRecordValue, error) { + refTemplate, ok := refs[refName] + if !ok { + return nil, fmt.Errorf("ref not found: %s", refName) + } + cpy := p.copyListRecord(&refTemplate) + return &cpy, nil +} + +func (p *redisYamlParser) resolveZSetReference(refs map[string]ZSetRecordValue, refName string) (*ZSetRecordValue, error) { + refTemplate, ok := refs[refName] + if !ok { + return nil, fmt.Errorf("ref not found: %s", refName) + } + cpy := p.copyZSetRecord(&refTemplate) + return &cpy, nil +} + +func (p *redisYamlParser) buildKeys(ctx *context, data *Keys) error { + if data == nil { + return nil + } + if err := p.extendKeys(ctx, data); err != nil { + return err + } + if data.Name != "" { + ctx.keyRefs[data.Name] = p.copyKeyRecord(data) + } + return nil +} + +func (p *redisYamlParser) buildSets(ctx *context, data *Sets) error { + if data == nil { + return nil + } + for _, v := range data.Values { + if err := p.extendSet(ctx, v); err != nil { + return fmt.Errorf("extend set error: %w", err) + } + if v.Name != "" { + ctx.setRefs[v.Name] = p.copySetRecord(v) + } + } + return nil +} + +func (p *redisYamlParser) buildMaps(ctx *context, data *Hashes) error { + if data == nil { + return nil + } + for _, v := range data.Values { + if err := p.extendHash(ctx, v); err != nil { + return fmt.Errorf("extend hash error: %w", err) + } + if v.Name != "" { + ctx.hashRefs[v.Name] = p.copyHashRecord(v) + } + } + return nil +} + +func (p *redisYamlParser) buildLists(ctx *context, data *Lists) error { + if data == nil { + return nil + } + for _, v := range data.Values { + if err := p.extendList(ctx, v); err != nil { + return fmt.Errorf("extend list error: %w", err) + } + if v.Name != "" { + ctx.listRefs[v.Name] = p.copyListRecord(v) + } + } + return nil +} + +func (p *redisYamlParser) buildZSets(ctx *context, data *ZSets) error { + if data == nil { + return nil + } + for _, v := range data.Values { + if err := p.extendZSet(ctx, v); err != nil { + return fmt.Errorf("extend zset error: %w", err) + } + if v.Name != "" { + ctx.zsetRefs[v.Name] = p.copyZSetRecord(v) + } + } + return nil +} + +func (p *redisYamlParser) Parse(ctx *context, filename string) (*Fixture, error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + var fixture Fixture + if err := yaml.Unmarshal(data, &fixture); err != nil { + return nil, err + } + + for _, parentFixture := range fixture.Inherits { + _, err := p.fileParser.ParseFiles(ctx, []string{parentFixture}) + if err != nil { + return nil, err + } + } + + if err = p.buildTemplate(ctx, fixture); err != nil { + return nil, err + } + + for _, databaseData := range fixture.Databases { + if err := p.buildKeys(ctx, databaseData.Keys); err != nil { + return nil, err + } + if err := p.buildMaps(ctx, databaseData.Hashes); err != nil { + return nil, err + } + if err := p.buildSets(ctx, databaseData.Sets); err != nil { + return nil, err + } + if err := p.buildLists(ctx, databaseData.Lists); err != nil { + return nil, err + } + if err := p.buildZSets(ctx, databaseData.ZSets); err != nil { + return nil, err + } + } + + return &fixture, nil +} diff --git a/fixtures/redis/redis.go b/fixtures/redis/redis.go new file mode 100644 index 0000000..46f7a6f --- /dev/null +++ b/fixtures/redis/redis.go @@ -0,0 +1,194 @@ +package redis + +import ( + "context" + + "github.com/go-redis/redis/v9" + "github.com/lamoda/gonkey/fixtures/redis/parser" +) + +type loader struct { + locations []string + client *redis.Client +} + +type LoaderOptions struct { + FixtureDir string + Redis *redis.Options +} + +func New(opts LoaderOptions) *loader { + client := redis.NewClient(opts.Redis) + return &loader{ + locations: []string{opts.FixtureDir}, + client: client, + } +} + +func (l *loader) Load(names []string) error { + ctx := parser.NewContext() + fileParser := parser.New(l.locations) + fixtureList, err := fileParser.ParseFiles(ctx, names) + if err != nil { + return err + } + return l.loadData(fixtureList) +} + +func (l *loader) loadKeys(ctx context.Context, pipe redis.Pipeliner, db parser.Database) error { + if db.Keys == nil { + return nil + } + for k, v := range db.Keys.Values { + if err := pipe.Set(ctx, k, v.Value.Value, v.Expiration).Err(); err != nil { + return err + } + } + return nil +} + +func (l *loader) loadSets(ctx context.Context, pipe redis.Pipeliner, db parser.Database) error { + if db.Sets == nil { + return nil + } + for setKey, setRecord := range db.Sets.Values { + values := make([]interface{}, 0, len(setRecord.Values)) + for _, v := range setRecord.Values { + values = append(values, v.Value.Value) + } + if err := pipe.SAdd(ctx, setKey, values).Err(); err != nil { + return err + } + if setRecord.Expiration > 0 { + if err := pipe.Expire(ctx, setKey, setRecord.Expiration).Err(); err != nil { + return err + } + } + } + return nil +} + +func (l *loader) loadHashes(ctx context.Context, pipe redis.Pipeliner, db parser.Database) error { + if db.Hashes == nil { + return nil + } + for key, record := range db.Hashes.Values { + values := make([]interface{}, 0, len(record.Values) * 2) + for _, v := range record.Values { + values = append(values, v.Key.Value, v.Value.Value) + } + if err := pipe.HSet(ctx, key, values...).Err(); err != nil { + return err + } + if record.Expiration > 0 { + if err := pipe.Expire(ctx, key, record.Expiration).Err(); err != nil { + return err + } + } + } + return nil +} + +func (l *loader) loadLists(ctx context.Context, pipe redis.Pipeliner, db parser.Database) error{ + if db.Lists == nil { + return nil + } + for key, record := range db.Lists.Values { + values := make([]interface{}, 0, len(record.Values)) + for _, v := range record.Values { + values = append(values, v.Value.Value) + } + if err := pipe.RPush(ctx, key, values...).Err(); err != nil { + return err + } + if record.Expiration > 0 { + if err := pipe.Expire(ctx, key, record.Expiration).Err(); err != nil { + return err + } + } + } + return nil +} + +func (l *loader) loadSortedSets(ctx context.Context, pipe redis.Pipeliner, db parser.Database) error { + if db.ZSets == nil { + return nil + } + for key, record := range db.ZSets.Values { + values := make([]redis.Z, 0, len(record.Values)) + for _, v := range record.Values { + values = append(values, redis.Z{ + Score: v.Score, + Member: v.Value.Value, + }) + } + if err := pipe.ZAdd(ctx, key, values...).Err(); err != nil { + return err + } + if record.Expiration > 0 { + if err := pipe.Expire(ctx, key, record.Expiration).Err(); err != nil { + return err + } + } + } + return nil +} + +func (l *loader) loadRedisDatabase(ctx context.Context, dbID int, db parser.Database, needTruncate bool) error { + pipe := l.client.Pipeline() + err := pipe.Select(ctx, dbID).Err() + if err != nil { + return err + } + + if needTruncate { + if err := pipe.FlushDB(ctx).Err(); err != nil { + return err + } + } + + if err := l.loadKeys(ctx, pipe, db); err != nil { + return err + } + + if err := l.loadSets(ctx, pipe, db); err != nil { + return err + } + + if err := l.loadHashes(ctx, pipe, db); err != nil { + return err + } + + if err := l.loadLists(ctx, pipe, db); err != nil { + return err + } + + if err := l.loadSortedSets(ctx, pipe, db); err != nil { + return err + } + + if _, err := pipe.Exec(ctx); err != nil { + return err + } + + return nil +} + +func (l *loader) loadData(fixtures []*parser.Fixture) error { + truncatedDatabases := make(map[int]struct{}) + + for _, redisFixture := range fixtures { + for dbID, db := range redisFixture.Databases { + var needTruncate bool + if _, ok := truncatedDatabases[dbID]; !ok { + truncatedDatabases[dbID] = struct{}{} + needTruncate = true + } + err := l.loadRedisDatabase(context.Background(), dbID, db, needTruncate) + if err != nil { + return err + } + } + } + return nil +} diff --git a/fixtures/testdata/redis.yaml b/fixtures/testdata/redis.yaml new file mode 100644 index 0000000..c2cf7c5 --- /dev/null +++ b/fixtures/testdata/redis.yaml @@ -0,0 +1,81 @@ +databases: + 1: + keys: + values: + key1: + value: value1 + key2: + expiration: 10s + value: value2 + sets: + values: + set1: + expiration: 10s + values: + - value: a + - value: b + - value: c + set3: + expiration: 5s + values: + - value: x + - value: y + hashes: + values: + map1: + values: + - key: a + value: 1 + - key: b + value: 2 + map2: + values: + - key: c + value: 3 + - key: d + value: 4 + lists: + values: + list1: + values: + - value: 1 + - value: "2" + - value: a + zsets: + values: + zset1: + values: + - value: 1 + score: 1.1 + - value: "2" + score: 5.6 + 2: + keys: + values: + key3: + value: value3 + key4: + expiration: 5s + value: value4 + sets: + values: + set2: + expiration: 5s + values: + - value: d + - value: e + - value: f + hashes: + values: + map3: + values: + - key: c + value: 3 + - key: d + value: 4 + map4: + values: + - key: e + value: 10 + - key: f + value: 11 diff --git a/fixtures/testdata/redis_example.yaml b/fixtures/testdata/redis_example.yaml new file mode 100644 index 0000000..7346d54 --- /dev/null +++ b/fixtures/testdata/redis_example.yaml @@ -0,0 +1,118 @@ +templates: + keys: + - $name: parentKeyTemplate + values: + baseKey: + expiration: 1s + value: 1 + - $name: childKeyTemplate + $extend: parentKeyTemplate + values: + otherKey: + value: 2 + sets: + - $name: parentSetTemplate + expiration: 10s + values: + - value: a + - $name: childSetTemplate + $extend: parentSetTemplate + values: + - value: b + hashes: + - $name: parentHashTemplate + values: + - key: a + value: 1 + - key: b + value: 2 + - $name: childHashTemplate + $extend: parentHashTemplate + values: + - key: c + value: 3 + - key: d + value: 4 + lists: + - $name: parentListTemplate + values: + - value: 1 + - value: 2 + - $name: childListTemplate + values: + - value: 3 + - value: 4 + zsets: + - $name: parentZSetTemplate + values: + - value: 1 + score: 2.1 + - value: 2 + score: 4.3 + - $name: childZSetTemplate + value: + - value: 3 + score: 6.5 + - value: 4 + score: 8.7 +databases: + 1: + keys: + $extend: childKeyTemplate + values: + key1: + value: value1 + key2: + expiration: 10s + value: value2 + sets: + values: + set1: + $extend: childSetTemplate + expiration: 10s + values: + - value: a + - value: b + set3: + expiration: 5s + values: + - value: x + - value: y + hashes: + values: + map1: + $extend: childHashTemplate + values: + - key: a + value: 1 + - key: b + value: 2 + map2: + values: + - key: c + value: 3 + - key: d + value: 4 + lists: + values: + list1: + $extend: childListTemplate + values: + - value: 1 + - value: 100 + - value: 200 + zsets: + values: + zset1: + $extend: childZSetTemplate + values: + - value: 5 + score: 10.1 + 2: + keys: + values: + key3: + value: value3 + key4: + expiration: 5s + value: value4 diff --git a/fixtures/testdata/redis_extend.yaml b/fixtures/testdata/redis_extend.yaml new file mode 100644 index 0000000..d397b9d --- /dev/null +++ b/fixtures/testdata/redis_extend.yaml @@ -0,0 +1,115 @@ +templates: + keys: + - $name: parentKeys + values: + a: + value: 1 + b: + value: 2 + - $name: childKeys + $extend: parentKeys + values: + c: + value: 3 + d: + value: 4 + sets: + - $name: parentSet + expiration: 10s + values: + - value: a + - value: b + - $name: childSet + $extend: parentSet + values: + - value: c + hashes: + - $name: parentMap + values: + - key: a1 + value: 1 + - key: b1 + value: 2 + - $name: childMap + $extend: parentMap + values: + - key: c1 + value: 3 + lists: + - $name: parentList + values: + - value: 1 + - value: 2 + - $name: childList + $extend: parentList + values: + - value: 3 + - value: 4 + zsets: + - $name: parentZSet + values: + - value: 1 + score: 1.2 + - value: 2 + score: 3.4 + - $name: childZSet + $extend: parentZSet + values: + - value: 3 + score: 5.6 + - value: 4 + score: 7.8 + +databases: + 1: + keys: + $extend: childKeys + values: + key1: + value: value1 + key2: + expiration: 10s + value: value2 + sets: + values: + set1: + $extend: childSet + values: + - value: d + set2: + expiration: 10s + values: + - value: x + - value: y + hashes: + values: + map1: + $extend: childMap + $name: baseMap + values: + - key: a + value: 1 + - key: b + value: 2 + map2: + values: + - key: c + value: 3 + - key: d + value: 4 + lists: + values: + list1: + $name: list1 + $extend: childList + values: + - value: 10 + - value: 11 + zsets: + values: + zset1: + $name: zset1 + $extend: childZSet + values: + - value: 5 + score: 10.1 diff --git a/fixtures/testdata/redis_inherits.yaml b/fixtures/testdata/redis_inherits.yaml new file mode 100644 index 0000000..628806f --- /dev/null +++ b/fixtures/testdata/redis_inherits.yaml @@ -0,0 +1,54 @@ +inherits: + - redis_extend +databases: + 1: + keys: + $extend: childKeys + values: + key1: + value: value1 + key2: + expiration: 10s + value: value2 + sets: + values: + set1: + $extend: childSet + hashes: + values: + map1: + $extend: baseMap + values: + - key: x + value: 10 + - key: y + value: 11 + map2: + $extend: childMap + values: + - key: t + value: 500 + - key: j + value: 1000 + lists: + values: + list2: + $extend: list1 + values: + - value: 100 + list3: + $extend: childList + values: + - value: 200 + zsets: + values: + zset2: + $extend: zset1 + values: + - value: 100 + score: 100.1 + zset3: + $extend: childZSet + values: + - value: 200 + score: 200.2 diff --git a/go.mod b/go.mod index 90fc9e1..35d8cae 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Masterminds/sprig/v3 v3.2.2 github.com/aerospike/aerospike-client-go/v5 v5.8.0 github.com/fatih/color v1.7.0 + github.com/go-redis/redis/v9 v9.0.0-beta.2 // indirect github.com/google/uuid v1.1.1 github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.13 // indirect @@ -15,7 +16,6 @@ require ( github.com/mattn/go-colorable v0.1.12 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/onsi/ginkgo v1.16.5 // indirect - github.com/onsi/gomega v1.19.0 // indirect github.com/stretchr/testify v1.7.1 github.com/tidwall/gjson v1.13.0 github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 // indirect @@ -23,4 +23,5 @@ require ( golang.org/x/sync v0.0.0-20210220032951-036812b2e83c gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f70ed36..b288ac5 100644 --- a/go.sum +++ b/go.sum @@ -6,17 +6,23 @@ github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmy github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/aerospike/aerospike-client-go/v5 v5.8.0 h1:EUV2wG80yIenQqOyUlf5NfyhagPIwoeL09MJIE+xILE= github.com/aerospike/aerospike-client-go/v5 v5.8.0/go.mod h1:rJ/KpmClE7kiBPfvAPrGw9WuNOiz8v2uKbQaUyYPXtI= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-redis/redis/v9 v9.0.0-beta.2 h1:ZSr84TsnQyKMAg8gnV+oawuQezeJR11/09THcWCQzr4= +github.com/go-redis/redis/v9 v9.0.0-beta.2/go.mod h1:Bldcd/M/bm9HbnNPi/LUtYBSD8ttcZYBMupwMXhdU0o= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -33,6 +39,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -70,12 +78,16 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.1.3 h1:e/3Cwtogj0HA+25nMP1jCMDIf8RtRYbGwGGuBIFztkc= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.20.0 h1:8W0cWlwFkflGPLltQvLRB7ZVD5HuP6ng320w2IS245Q= +github.com/onsi/gomega v1.20.0/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= @@ -95,6 +107,7 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ= github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= @@ -102,21 +115,27 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -143,8 +162,12 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -156,6 +179,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -170,6 +194,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY= gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= @@ -185,3 +210,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 01c818f..c8621be 100644 --- a/main.go +++ b/main.go @@ -5,16 +5,19 @@ import ( "errors" "flag" "log" + "net/url" "os" "strconv" "strings" "github.com/aerospike/aerospike-client-go/v5" + "github.com/go-redis/redis/v9" "github.com/joho/godotenv" "github.com/lamoda/gonkey/checker/response_body" "github.com/lamoda/gonkey/checker/response_db" "github.com/lamoda/gonkey/fixtures" + redisLoader "github.com/lamoda/gonkey/fixtures/redis" "github.com/lamoda/gonkey/output/allure_report" "github.com/lamoda/gonkey/output/console_colored" "github.com/lamoda/gonkey/runner" @@ -28,6 +31,7 @@ type config struct { TestsLocation string DbDsn string AerospikeHost string + RedisAddr string FixturesLocation string EnvFile string Allure bool @@ -49,11 +53,11 @@ func main() { fixturesLoader := initLoaders(storages, cfg) - runner := initRunner(cfg, fixturesLoader) + runnerInstance := initRunner(cfg, fixturesLoader) - addCheckers(runner, storages.db) + addCheckers(runnerInstance, storages.db) - run(runner, cfg) + run(runnerInstance, cfg) } func initStorages(cfg config) storages { @@ -67,16 +71,25 @@ func initStorages(cfg config) storages { func initLoaders(storages storages, cfg config) fixtures.Loader { var fixturesLoader fixtures.Loader - if (storages.db != nil || storages.aerospike != nil) && cfg.FixturesLocation != "" { - fixturesLoader = fixtures.NewLoader(&fixtures.Config{ - DB: storages.db, - Aerospike: storages.aerospike, - Location: cfg.FixturesLocation, - Debug: cfg.Debug, - DbType: fixtures.FetchDbType(cfg.DbType), - }) - } else if cfg.FixturesLocation != "" { - log.Fatal(errors.New("you should specify db_dsn to load fixtures")) + if cfg.FixturesLocation != "" { + if storages.db != nil || storages.aerospike != nil { + fixturesLoader = fixtures.NewLoader(&fixtures.Config{ + DB: storages.db, + Aerospike: storages.aerospike, + Location: cfg.FixturesLocation, + Debug: cfg.Debug, + DbType: fixtures.FetchDbType(cfg.DbType), + }) + } else if cfg.DbType == fixtures.RedisParam { + fixturesLoader = redisLoader.New(redisLoader.LoaderOptions{ + FixtureDir: cfg.FixturesLocation, + Redis: &redis.Options{ + Addr: cfg.RedisAddr, + }, + }) + } else { + log.Fatal(errors.New("you should specify db_dsn to load fixtures")) + } } return fixturesLoader } @@ -100,6 +113,13 @@ func validateConfig(cfg *config) { log.Println(errors.New("can't load .env file"), err) } } + + if cfg.RedisAddr != "" { + _, err := url.Parse(cfg.RedisAddr) + if err != nil { + log.Fatal("redis_addr attribute is not a valid URL address") + } + } } func addCheckers(r *runner.Runner, db *sql.DB) { @@ -179,6 +199,7 @@ func getConfig() config { flag.StringVar(&cfg.TestsLocation, "tests", "", "Path to tests file or directory") flag.StringVar(&cfg.DbDsn, "db_dsn", "", "DSN for the fixtures database (WARNING! Db tables will be truncated)") flag.StringVar(&cfg.AerospikeHost, "aerospike_host", "", "Aerospike host for fixtures in form of 'host:port/namespace' (WARNING! Aerospike sets will be truncated)") + flag.StringVar(&cfg.RedisAddr, "redis_addr", "", "Redis server URL for fixture loading") flag.StringVar(&cfg.FixturesLocation, "fixtures", "", "Path to fixtures directory") flag.StringVar(&cfg.EnvFile, "env-file", "", "Path to env-file") flag.BoolVar(&cfg.Allure, "allure", true, "Make Allure report") @@ -188,7 +209,7 @@ func getConfig() config { &cfg.DbType, "db-type", fixtures.PostgresParam, - "Type of database (options: postgres, mysql, aerospike)", + "Type of database (options: postgres, mysql, aerospike, redis)", ) flag.Parse() diff --git a/runner/runner_testing.go b/runner/runner_testing.go index bb03ef3..ac3bfa0 100644 --- a/runner/runner_testing.go +++ b/runner/runner_testing.go @@ -29,17 +29,18 @@ type Aerospike struct { } type RunWithTestingParams struct { - Server *httptest.Server - TestsDir string - Mocks *mocks.Mocks - FixturesDir string - DB *sql.DB - Aerospike Aerospike + Server *httptest.Server + TestsDir string + Mocks *mocks.Mocks + FixturesDir string + DB *sql.DB + Aerospike Aerospike // If DB parameter present, used to recognize type of database, if not set, by default uses Postgres - DbType fixtures.DbType - EnvFilePath string - OutputFunc output.OutputInterface - Checkers []checker.CheckerInterface + DbType fixtures.DbType + EnvFilePath string + OutputFunc output.OutputInterface + Checkers []checker.CheckerInterface + FixtureLoader fixtures.Loader } // RunWithTesting is a helper function the wraps the common Run and provides simple way @@ -59,13 +60,14 @@ func RunWithTesting(t *testing.T, params *RunWithTestingParams) { debug := os.Getenv("GONKEY_DEBUG") != "" var fixturesLoader fixtures.Loader - if params.DB != nil || params.Aerospike.Client != nil { + if params.DB != nil || params.Aerospike.Client != nil || params.FixtureLoader != nil { fixturesLoader = fixtures.NewLoader(&fixtures.Config{ - Location: params.FixturesDir, - DB: params.DB, - Aerospike: aerospikeAdapter.New(params.Aerospike.Client, params.Aerospike.Namespace), - Debug: debug, - DbType: params.DbType, + Location: params.FixturesDir, + DB: params.DB, + Aerospike: aerospikeAdapter.New(params.Aerospike.Client, params.Aerospike.Namespace), + Debug: debug, + DbType: params.DbType, + FixtureLoader: params.FixtureLoader, }) }