From a36469a994e337274e17af09dc01c8625dee5850 Mon Sep 17 00:00:00 2001
From: Emmanuel Raviart <emmanuel@raviart.com>
Date: Sun, 24 Sep 2023 10:53:48 +0200
Subject: [PATCH] Add cache for test cases simulations

---
 example.env                                  |  7 ++-
 src/lib/hash.ts                              | 20 -------
 src/lib/server/auditors/config.ts            |  1 +
 src/lib/server/config.ts                     |  2 +
 src/routes/+layout.svelte                    |  4 +-
 src/routes/budgets/+server.ts                | 37 ++++++++++---
 src/routes/test_cases/simulations/+server.ts | 57 ++++++++++++++++++++
 7 files changed, 95 insertions(+), 33 deletions(-)
 create mode 100644 src/routes/test_cases/simulations/+server.ts

diff --git a/example.env b/example.env
index 2ca2ab2be..a0c6a9b3f 100644
--- a/example.env
+++ b/example.env
@@ -86,11 +86,14 @@ PROXY=false
 # Show intro balloons.
 SHOW_TUTORIAL=true
 
+# Directory containing cache of budget simulations
+SIMULATIONS_BUDGET_DIR="../simulations_budget"
+
 # Directory containing JSON of simulations saved by users
 SIMULATIONS_DIR="../simulations"
 
-# Directory containing JSON of budget simulations saved by users
-SIMULATIONS_BUDGET_DIR="../simulations_budget"
+# Directory containing cache of test cases simulations
+SIMULATIONS_TEST_CASES_DIR="../simulations_cas_types"
 
 # Key for taxable household (aka "foyer fiscal") entity
 TAXABLE_HOUSEHOLD_KEY="foyer_fiscal"
diff --git a/src/lib/hash.ts b/src/lib/hash.ts
index a7d524b77..a0f845b8e 100644
--- a/src/lib/hash.ts
+++ b/src/lib/hash.ts
@@ -1,8 +1,5 @@
 import { XXH64 } from "xxh3-ts"
 
-import type { ParametricReform } from "$lib/reforms"
-import { budgetEditableParametersName } from "$lib/variables"
-
 // Sometimes, the magnitude of the BigInt value generated by XXH64()
 // is not large enough to require 16 characters when represented in base 16.
 // As a result, the base 16 representation has leading zeros that are not necessary,
@@ -18,20 +15,3 @@ export function hashObject(obj: object): string {
     .toString(16)
     .padStart(16, "0")
 }
-
-export function hashBudgetSimulationCache(
-  parametricReform: ParametricReform,
-  outputVariables: string[],
-): string {
-  return hashObject({
-    parametricReform: Object.entries(parametricReform)
-      .sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
-      .reduce((filtered: ParametricReform, [parameterName, value]) => {
-        if (budgetEditableParametersName.has(parameterName)) {
-          filtered[parameterName] = value
-        }
-        return filtered
-      }, {}),
-    outputVariables: outputVariables.sort().join("."),
-  })
-}
diff --git a/src/lib/server/auditors/config.ts b/src/lib/server/auditors/config.ts
index 587ff0c3d..564a457d3 100644
--- a/src/lib/server/auditors/config.ts
+++ b/src/lib/server/auditors/config.ts
@@ -75,6 +75,7 @@ export function auditConfig(
     "jwtSecret",
     "simulationsBudgetDir",
     "simulationsDir",
+    "simulationsTestCasesDir",
     "taxableHouseholdEntityKey",
     "title",
   ]) {
diff --git a/src/lib/server/config.ts b/src/lib/server/config.ts
index 4ba7a1801..e44521115 100644
--- a/src/lib/server/config.ts
+++ b/src/lib/server/config.ts
@@ -37,6 +37,7 @@ export interface Config {
   revaluationName?: string
   showTutorial?: boolean
   simulationsBudgetDir: string
+  simulationsTestCasesDir: string
   simulationsDir: string
   taxableHouseholdEntityKey: string
   territoiresUrl: string
@@ -89,6 +90,7 @@ const [validConfig, error] = validateConfig({
   showTutorial: process.env["SHOW_TUTORIAL"],
   simulationsBudgetDir: process.env["SIMULATIONS_BUDGET_DIR"],
   simulationsDir: process.env["SIMULATIONS_DIR"],
+  simulationsTestCasesDir: process.env["SIMULATIONS_TEST_CASES_DIR"],
   taxableHouseholdEntityKey: process.env["TAXABLE_HOUSEHOLD_KEY"],
   territoiresUrl: process.env["TERRITOIRES_URL"],
   title: process.env["TITLE"],
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 42d8dfc46..bafbc2ab1 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -821,9 +821,7 @@
     token: string
     variables: string[]
   }) {
-    const apiBaseUrlIndex = Math.floor(Math.random() * apiBaseUrls.length)
-    const apiBaseUrl = apiBaseUrls[apiBaseUrlIndex]
-    const response = await fetch(new URL("simulations", apiBaseUrl), {
+    const response = await fetch("/test_cases/simulations", {
       body: JSON.stringify({
         period,
         parametric_reform,
diff --git a/src/routes/budgets/+server.ts b/src/routes/budgets/+server.ts
index ad690185e..3d36204dd 100644
--- a/src/routes/budgets/+server.ts
+++ b/src/routes/budgets/+server.ts
@@ -3,17 +3,19 @@ import jwt from "jsonwebtoken"
 import path from "path"
 import { error, json } from "@sveltejs/kit"
 
-import { hashBudgetSimulationCache } from "$lib/hash"
+import { hashObject } from "$lib/hash"
 import { getParameter, rootParameter } from "$lib/parameters"
 import config from "$lib/server/config"
+import type { ParametricReform } from "$lib/reforms"
 import type { CachedSimulation } from "$lib/simulations"
 import type { User } from "$lib/users"
+import { budgetEditableParametersName } from "$lib/variables"
 
 import type { RequestHandler } from "./$types"
 
 const { simulationsBudgetDir } = config
 
-function getPath(digest: string) {
+function getCacheFilePath(digest: string) {
   return path.join(
     simulationsBudgetDir,
     digest.substring(0, 2),
@@ -21,6 +23,25 @@ function getPath(digest: string) {
   )
 }
 
+function hashBudgetSimulationCache(
+  parametricReform: ParametricReform,
+  outputVariables: string[],
+): string {
+  return hashObject({
+    parametricReform: Object.entries(parametricReform)
+      .sort(([parameterNameA], [parameterNameB]) =>
+        parameterNameA.localeCompare(parameterNameB),
+      )
+      .reduce((filtered: ParametricReform, [parameterName, value]) => {
+        if (budgetEditableParametersName.has(parameterName)) {
+          filtered[parameterName] = value
+        }
+        return filtered
+      }, {}),
+    outputVariables: outputVariables.sort().join("."),
+  })
+}
+
 export const POST: RequestHandler = async ({ fetch, locals, request }) => {
   if (
     config.budgetApiUrl === undefined ||
@@ -62,30 +83,30 @@ export const POST: RequestHandler = async ({ fetch, locals, request }) => {
     {},
     requestJson.output_variables,
   )
-  const baseCachePath = getPath(baseCacheDigest)
+  const baseCacheFilePath = getCacheFilePath(baseCacheDigest)
 
   // Path of the current reform cache
-  const cachePath = getPath(cacheDigest)
+  const cacheFilePath = getCacheFilePath(cacheDigest)
 
   const isSimulationPublic = indexContents.find(
     (cachedSimulation) => cachedSimulation.hash === cacheDigest,
   )?.public
 
   if (
-    (await fs.pathExists(cachePath)) &&
+    (await fs.pathExists(cacheFilePath)) &&
     (user !== undefined || isSimulationPublic)
   ) {
     return json({
       errors: [],
-      result: (await fs.readJson(cachePath)).budgetSimulation,
+      result: (await fs.readJson(cacheFilePath)).budgetSimulation,
       hash: cacheDigest,
       isPublic: isSimulationPublic,
     })
   } else if (user === undefined) {
-    if (await fs.pathExists(baseCachePath)) {
+    if (await fs.pathExists(baseCacheFilePath)) {
       return json({
         errors: [],
-        result: (await fs.readJson(baseCachePath)).budgetSimulation,
+        result: (await fs.readJson(baseCacheFilePath)).budgetSimulation,
         hash: baseCacheDigest,
         isPublic: true,
       })
diff --git a/src/routes/test_cases/simulations/+server.ts b/src/routes/test_cases/simulations/+server.ts
new file mode 100644
index 000000000..05099c336
--- /dev/null
+++ b/src/routes/test_cases/simulations/+server.ts
@@ -0,0 +1,57 @@
+import fs from "fs-extra"
+import path from "path"
+import { json } from "@sveltejs/kit"
+
+import { hashObject } from "$lib/hash"
+import config from "$lib/server/config"
+
+import type { RequestHandler } from "./$types"
+
+const { apiBaseUrls, simulationsTestCasesDir } = config
+
+export const POST: RequestHandler = async ({ fetch, request }) => {
+  const input = await request.clone().json()
+
+  const digest = hashObject({
+    period: input.period,
+    parametric_reform: Object.entries(input.parametric_reform ?? {}).sort(
+      ([parameterNameA], [parameterNameB]) =>
+        parameterNameA.localeCompare(parameterNameB),
+    ),
+    reform: input.reform,
+    situation: input.situation,
+    // Ignore title.
+    // Ignore token.
+    variables: input.variables.sort(),
+  })
+  const cacheDir = path.join(simulationsTestCasesDir, digest.substring(0, 2))
+  const cacheFilePath = path.join(cacheDir, `${digest}.json`)
+
+  if (await fs.pathExists(cacheFilePath)) {
+    return json((await fs.readJson(cacheFilePath)).output)
+  }
+
+  const apiBaseUrlIndex = Math.floor(Math.random() * apiBaseUrls.length)
+  const apiBaseUrl = apiBaseUrls[apiBaseUrlIndex]
+  const response = await fetch(new URL("simulations", apiBaseUrl), {
+    body: JSON.stringify(input),
+    headers: {
+      Accept: "application/json",
+      "Content-Type": "application/json; charset=utf-8",
+    },
+    method: "POST",
+  })
+  if (!response.ok) {
+    return response
+  }
+  const output = await response.json()
+  if (!(await fs.pathExists(cacheFilePath))) {
+    await fs.ensureDir(cacheDir)
+    await fs.writeJson(
+      cacheFilePath,
+      { digest, input, output },
+      { encoding: "utf-8", spaces: 2 },
+    )
+  }
+  return json(output)
+}
-- 
GitLab