j126: read bot settings from a .env file (default ./.env, --env-file flag, TR_ENV_FILE); file wins over shell leftovers, boot report labels (source: .env)
This commit is contained in:
@@ -22,6 +22,9 @@ __pycache__/
|
||||
devbox.json
|
||||
devbox.lock
|
||||
|
||||
# Local settings file (explicit path only - never a wildcard pattern)
|
||||
ModularBot_garage/.env
|
||||
|
||||
# Compiled bot binaries — all builds go to *_garage/out/
|
||||
*_garage/out/
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Copy this file to `.env` (same folder as the bot) and edit what you need.
|
||||
# The bot reads `.env` automatically when you start it from its folder.
|
||||
# To use a different file: ./out/ModularBot --env-file /path/to/my.env
|
||||
#
|
||||
# The file WINS over variables you exported in the shell. If a shell value
|
||||
# differs, the bot prints a warning line so you know it was overridden.
|
||||
#
|
||||
# Everything is optional. Leave a line out (or commented, with #) to keep the
|
||||
# built-in default. Values shown are the defaults.
|
||||
|
||||
# ── movement ─────────────────────────────────────────────────────────────────
|
||||
# Which dodging engine to run: strafe (default), tfil, tfil_ring, surf.
|
||||
TR_MOVEMENT=strafe
|
||||
|
||||
# ── gun rack: which guns the bot may choose (off | 1v1 | melee | both) ───────
|
||||
# The shipped default is Pattern only. Turn a gun on with `both`.
|
||||
TR_RACK_PATTERN=both
|
||||
#TR_RACK_TMHORIZON=both
|
||||
#TR_RACK_HEADON=both
|
||||
|
||||
# Drop whole guns by number (comma separated ids; empty = keep all).
|
||||
GUN_RACK_DISABLE=
|
||||
|
||||
# ── power / energy policy ────────────────────────────────────────────────────
|
||||
# 1 = use the energy-aware power cap (default). 0 = no cap (for experiments).
|
||||
TR_POWER_POLICY=1
|
||||
# Lowest cap when our own energy is low. NOTE: this is a CEILING, not a floor.
|
||||
TR_POWER_ENERGY_MIN=0.5
|
||||
|
||||
# ── logs (set the value to 1; presence alone turns some of them on) ──────────
|
||||
# Print one line per power decision.
|
||||
#TR_POWER_LOG=1
|
||||
# Print one line per ram start/stop and why.
|
||||
#TR_RAM_LOG=1
|
||||
# Print movement band/class changes.
|
||||
#TR_MOVEMENT_LOG=1
|
||||
# Print the per-shot thinking of the TM horizon gun.
|
||||
#TR_TMHORIZON_LOG=1
|
||||
# Print one line per round result.
|
||||
TR_RESULT_LOG=1
|
||||
|
||||
# Where the per-round gun stats and the per-shot log are written.
|
||||
GUN_STATS_PATH=/tmp/gun_stats.jsonl
|
||||
GUN_SHOTLOG_PATH=/tmp/shot_log.jsonl
|
||||
|
||||
# ── measurement helpers (leave off unless you are measuring) ─────────────────
|
||||
#TR_RECORD_WORLDSTATE=1
|
||||
#TR_RADAR_SCANLOG=1
|
||||
#TR_TRACKER_PROBE=1
|
||||
|
||||
# ── boot report ──────────────────────────────────────────────────────────────
|
||||
# 1 = print the [env] report on startup (default). 0 = do not print it.
|
||||
TR_ENV_REPORT=1
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname "$0")"
|
||||
exec ./out/ModularBot >> /tmp/modularbot_stdout.log 2>> /tmp/modularbot_stderr.log
|
||||
exec ./out/ModularBot "$@" >> /tmp/modularbot_stdout.log 2>> /tmp/modularbot_stderr.log
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname "$0")"
|
||||
exec ./out/ModularBot >> /tmp/modularbot_stdout.log 2>> /tmp/modularbot_stderr.log
|
||||
exec ./out/ModularBot "$@" >> /tmp/modularbot_stdout.log 2>> /tmp/modularbot_stderr.log
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
## Radar: RadarLockModule (1v1) / AdaptiveMeleeRadarModule (2+ enemies), auto-switched per tick.
|
||||
## Movement: OscillatorModule (perpendicular strafing).
|
||||
|
||||
import env_boot # MUST be first: applies the .env file before any other
|
||||
# module's top-level `let` reads the environment.
|
||||
import std/[math, os, strformat, tables, sets, json, random, strutils]
|
||||
import robocode_tankroyale_botapi
|
||||
import radar_harness/radar_interface
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
## Imported FIRST by ModularBot.nim, purely for the module-init side effect
|
||||
## below. Nim runs an imported module's top-level code before the importer's,
|
||||
## and sibling imports in the order they are listed, so:
|
||||
##
|
||||
## import env_boot # <-- .env applied HERE
|
||||
## import robocode_tankroyale_botapi
|
||||
## import movements/... # <-- their top-level `let`s now see the file
|
||||
##
|
||||
## That ordering is the whole point: it is what lets every existing env reader
|
||||
## in every other module keep working unchanged. Do not reorder this import.
|
||||
|
||||
import env_dotenv
|
||||
|
||||
bootDotEnv()
|
||||
@@ -0,0 +1,201 @@
|
||||
## .env file support for the bot.
|
||||
##
|
||||
## Why: a dozen TR_* variables were exported in the shell that launched the
|
||||
## server/GUI, and stale leftovers were repeatedly mistaken for live settings
|
||||
## (e.g. a forgotten TR_RACK_TMHORIZON). This module lets those settings live in
|
||||
## a file instead — and the FILE WINS over a leftover shell export.
|
||||
##
|
||||
## Values are applied with putEnv() before anything else reads the environment,
|
||||
## so every existing reader (getEnv, existsEnv, the envInt/envFloat/envBool
|
||||
## helpers, the PRESENCE-based knobs like TR_POWER_LOG) keeps working unchanged.
|
||||
##
|
||||
## Resolution order (first hit wins):
|
||||
## 1. --env-file <path> (also --env-file=<path>)
|
||||
## 2. TR_ENV_FILE
|
||||
## 3. ./.env in the current working directory (the bot root when launched
|
||||
## from the bot dir, which the launcher scripts do)
|
||||
## 4. .env next to the executable
|
||||
## 5. none
|
||||
##
|
||||
## An EXPLICIT request (1 or 2) that names a missing file is a hard error: an
|
||||
## explicit request that silently does nothing is exactly the bug this fixes.
|
||||
## A missing default file (3/4) is normal and silent.
|
||||
##
|
||||
## The boot report (env_report.nim) reads the resolved path/source and the set
|
||||
## of keys that came from the file, so it can label values `(source: .env)`.
|
||||
|
||||
import std/[os, strutils, sets]
|
||||
|
||||
const EnvFileFlag* = "--env-file"
|
||||
const EnvFileEnvVar* = "TR_ENV_FILE"
|
||||
const DefaultEnvFile* = ".env"
|
||||
|
||||
type
|
||||
EnvFileError* = object of CatchableError
|
||||
## Raised when an explicitly requested env file cannot be used.
|
||||
|
||||
EnvEntry* = object
|
||||
key*: string
|
||||
value*: string
|
||||
|
||||
EnvConflict* = object
|
||||
## A key present in BOTH the file and the real environment with different
|
||||
## values. The file value is the one applied; `shellValue` is what it
|
||||
## replaced (the stale leftover that used to win by accident).
|
||||
key*: string
|
||||
fileValue*: string
|
||||
shellValue*: string
|
||||
|
||||
EnvFileChoice* = object
|
||||
path*: string ## resolved path; "" means "no file"
|
||||
source*: string ## "flag" | "TR_ENV_FILE" | "cwd" | "exe" | "none"
|
||||
explicit*: bool ## requested via flag/env var (missing => hard error)
|
||||
|
||||
# ── resolved state (read by the boot report) ─────────────────────────────────
|
||||
|
||||
var
|
||||
gEnvFilePath = ""
|
||||
gEnvFileSource = "none"
|
||||
gEnvFileKeys: HashSet[string]
|
||||
gEnvFileLoaded = false
|
||||
|
||||
proc envFilePath*(): string = gEnvFilePath
|
||||
proc envFileSource*(): string = gEnvFileSource
|
||||
proc isFromEnvFile*(key: string): bool = key in gEnvFileKeys
|
||||
|
||||
# ── CLI parsing ──────────────────────────────────────────────────────────────
|
||||
|
||||
proc flagEnvFilePath*(params: openArray[string] = commandLineParams()): string =
|
||||
## The value of `--env-file` / `--env-file=`, or "" when absent. Raises when
|
||||
## `--env-file` is the last argument (a request we cannot honour).
|
||||
var i = 0
|
||||
while i < params.len:
|
||||
let p = params[i]
|
||||
if p == EnvFileFlag:
|
||||
if i + 1 < params.len:
|
||||
return params[i + 1]
|
||||
raise newException(EnvFileError, EnvFileFlag & " needs a path argument")
|
||||
elif p.startsWith(EnvFileFlag & "="):
|
||||
return p[EnvFileFlag.len + 1 .. ^1]
|
||||
inc i
|
||||
return ""
|
||||
|
||||
# ── resolution ───────────────────────────────────────────────────────────────
|
||||
|
||||
proc chooseEnvFile*(flagValue, envValue, cwdCandidate, exeCandidate: string): EnvFileChoice =
|
||||
## Pure resolution: flag, then TR_ENV_FILE, then `./.env`, then the `.env`
|
||||
## next to the executable, then none.
|
||||
if flagValue.len > 0:
|
||||
return EnvFileChoice(path: flagValue, source: "flag", explicit: true)
|
||||
if envValue.len > 0:
|
||||
return EnvFileChoice(path: envValue, source: EnvFileEnvVar, explicit: true)
|
||||
if fileExists(cwdCandidate):
|
||||
return EnvFileChoice(path: cwdCandidate, source: "cwd", explicit: false)
|
||||
if fileExists(exeCandidate):
|
||||
return EnvFileChoice(path: exeCandidate, source: "exe", explicit: false)
|
||||
return EnvFileChoice(path: "", source: "none", explicit: false)
|
||||
|
||||
# ── parsing ──────────────────────────────────────────────────────────────────
|
||||
|
||||
proc parseEnvFileContent*(content, filename: string): seq[EnvEntry] =
|
||||
## Parse the usual .env shape: one KEY=VALUE per line, blank lines and `#`
|
||||
## comments skipped, a leading `export ` tolerated, surrounding single or
|
||||
## double quotes stripped, whitespace trimmed, an empty VALUE allowed.
|
||||
##
|
||||
## A malformed line RAISES with the file name and line number — it is never
|
||||
## silently ignored, because a typo that does nothing is the failure mode we
|
||||
## are fixing.
|
||||
var lineno = 0
|
||||
for raw in content.splitLines():
|
||||
inc lineno
|
||||
var line = raw
|
||||
if line.len > 0 and line[^1] == '\r': line.setLen(line.len - 1)
|
||||
let stripped = line.strip()
|
||||
if stripped.len == 0 or stripped[0] == '#': continue
|
||||
var body = stripped
|
||||
if body.startsWith("export") and (body.len == 6 or body[6] in Whitespace):
|
||||
body = body[6 .. ^1].strip()
|
||||
let eq = body.find('=')
|
||||
if eq < 0:
|
||||
raise newException(ValueError,
|
||||
filename & ":" & $lineno & ": expected KEY=VALUE, got: " & stripped)
|
||||
let key = body[0 ..< eq].strip()
|
||||
if key.len == 0:
|
||||
raise newException(ValueError,
|
||||
filename & ":" & $lineno & ": empty key in: " & stripped)
|
||||
var value = body[eq + 1 .. ^1].strip()
|
||||
if value.len >= 2 and
|
||||
((value[0] == '"' and value[^1] == '"') or
|
||||
(value[0] == '\'' and value[^1] == '\'')):
|
||||
value = value[1 ..< value.len - 1]
|
||||
result.add EnvEntry(key: key, value: value)
|
||||
|
||||
# ── applying ─────────────────────────────────────────────────────────────────
|
||||
|
||||
proc applyEnvFile*(path: string): seq[EnvConflict] =
|
||||
## Read `path`, apply every entry with putEnv (the FILE WINS over the real
|
||||
## environment), remember the from-file keys, and return the keys whose file
|
||||
## value replaced a DIFFERENT exported value. Raises on a read error or a
|
||||
## malformed line.
|
||||
let entries = parseEnvFileContent(readFile(path), path)
|
||||
for e in entries:
|
||||
if existsEnv(e.key):
|
||||
let shell = getEnv(e.key)
|
||||
if shell != e.value:
|
||||
result.add EnvConflict(key: e.key, fileValue: e.value, shellValue: shell)
|
||||
putEnv(e.key, e.value)
|
||||
gEnvFileKeys.incl e.key
|
||||
|
||||
proc loadEnvFile*(choice: EnvFileChoice): seq[EnvConflict] =
|
||||
## Apply the chosen file. A missing EXPLICIT file raises EnvFileError; a
|
||||
## missing default file is a silent no-op. No file at all is a silent no-op.
|
||||
if choice.path.len == 0: return @[]
|
||||
if not fileExists(choice.path):
|
||||
if choice.explicit:
|
||||
raise newException(EnvFileError, "env file not found: " & choice.path)
|
||||
return @[]
|
||||
result = applyEnvFile(choice.path)
|
||||
|
||||
proc bootDotEnv*() =
|
||||
## Resolve and apply the .env file exactly once. Called at process start by
|
||||
## env_boot.nim (imported first by ModularBot.nim) so every later module-level
|
||||
## read sees the file values.
|
||||
if gEnvFileLoaded: return
|
||||
gEnvFileLoaded = true
|
||||
|
||||
var flag = ""
|
||||
try:
|
||||
flag = flagEnvFilePath()
|
||||
except EnvFileError as e:
|
||||
stderr.writeLine "[dotenv] ERROR: " & e.msg
|
||||
quit(1)
|
||||
|
||||
let envValue = if existsEnv(EnvFileEnvVar): getEnv(EnvFileEnvVar).strip() else: ""
|
||||
let choice = chooseEnvFile(flag, envValue,
|
||||
getCurrentDir() / DefaultEnvFile,
|
||||
getAppDir() / DefaultEnvFile)
|
||||
|
||||
if choice.path.len == 0:
|
||||
gEnvFilePath = ""
|
||||
gEnvFileSource = "none"
|
||||
return
|
||||
|
||||
try:
|
||||
let conflicts = loadEnvFile(choice)
|
||||
gEnvFilePath = choice.path
|
||||
gEnvFileSource = choice.source
|
||||
for c in conflicts:
|
||||
stderr.writeLine "[dotenv] " & c.key & " = " & c.fileValue &
|
||||
" (from " & choice.path & ") overrides the exported value " & c.shellValue
|
||||
except EnvFileError as e:
|
||||
stderr.writeLine "[dotenv] ERROR: " & e.msg
|
||||
stderr.writeLine "[dotenv] it was requested explicitly (--env-file or " &
|
||||
EnvFileEnvVar & "); refusing to start with the request silently ignored."
|
||||
quit(1)
|
||||
except ValueError as e:
|
||||
stderr.writeLine "[dotenv] ERROR: " & e.msg
|
||||
quit(1)
|
||||
except CatchableError as e:
|
||||
stderr.writeLine "[dotenv] ERROR: cannot use env file " & choice.path &
|
||||
": " & e.msg
|
||||
quit(1)
|
||||
@@ -20,6 +20,7 @@
|
||||
## printed, because unrelated variables may hold secrets or machine paths.
|
||||
|
||||
import std/[os, strutils, algorithm, times, sets]
|
||||
import env_dotenv
|
||||
import gun_harness/virtual_bullets
|
||||
import gun_harness/selector
|
||||
import movements/ram_decision
|
||||
@@ -89,13 +90,19 @@ proc tieBreakName(t: TieBreakMode): string =
|
||||
proc sourceOf(name: string): string =
|
||||
## Parsed-value knobs go through `getEnv(name, "")`; an empty value means the
|
||||
## shipped default, so a non-empty value is the only thing that counts as
|
||||
## "from the env".
|
||||
if getEnv(name, "").len > 0: "env" else: "default"
|
||||
## "from the env". A value that came from the resolved .env file is labelled
|
||||
## `.env` even though the file applied it with putEnv (so `getEnv` finds it).
|
||||
if isFromEnvFile(name): ".env"
|
||||
elif getEnv(name, "").len > 0: "env"
|
||||
else: "default"
|
||||
|
||||
proc sourceOfPresence(name: string): string =
|
||||
## Flags read with `existsEnv` are "from the env" whenever they are present,
|
||||
## even if their value is "0" (presence is what the code tests).
|
||||
if existsEnv(name): "env" else: "default"
|
||||
## even if their value is "0" (presence is what the code tests). A value that
|
||||
## came from the .env file is labelled `.env`.
|
||||
if isFromEnvFile(name): ".env"
|
||||
elif existsEnv(name): "env"
|
||||
else: "default"
|
||||
|
||||
proc disabledGunsString(guns: HashSet[int]): string =
|
||||
if guns.len == 0: return "none"
|
||||
@@ -461,6 +468,8 @@ proc knownEnvNames*(): seq[string] =
|
||||
"TR_TRACKER_PROBE", "TR_TRACKER_PROBE_PATH", "TR_VBULLET_ADMIT_ONLY",
|
||||
"TR_POWER_LOG",
|
||||
"TR_RESULT_LOG",
|
||||
# the .env loader's own environment hook (TR_ENV_FILE names the file)
|
||||
"TR_ENV_FILE",
|
||||
"TR_RAM_OPPORTUNITY", "TR_RAM_OPP_DIST", "TR_RAM_OPP_MARGIN",
|
||||
"TR_RAM_ABORT_DMG", "TR_RAM_PLAN", "TR_RAM_PLAN_DIST",
|
||||
"TR_RAM_PLAN_MARGIN", "TR_RAM_PLAN_HITRATE", "TR_RAM_LOG",
|
||||
@@ -609,6 +618,9 @@ proc printEnvReport*(ctx: EnvReportContext) =
|
||||
return
|
||||
|
||||
echo "[env] === ENVIRONMENT (boot report) ==="
|
||||
echo "[env] env file: ",
|
||||
(if envFilePath().len > 0: envFilePath() & " (source: " & envFileSource() & ")"
|
||||
else: "none")
|
||||
printRawEnvironment()
|
||||
printMistakeWarnings()
|
||||
printEffectiveValues(ctx)
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
## Guard test for the .env loader (the module the bot uses to read its
|
||||
## settings from a `.env` file).
|
||||
##
|
||||
## NO battle, NO Java, NO server. Run with:
|
||||
## nim c -r --path:common_libs common_libs/tests/test_env_dotenv.nim
|
||||
##
|
||||
## It pins the pieces that must not silently drift:
|
||||
## * the parser (comments, `export `, quotes, empty value, CRLF, and a
|
||||
## malformed line that MUST raise with the file+line);
|
||||
## * resolution precedence (flag > TR_ENV_FILE > ./.env > .env next to the
|
||||
## executable > none);
|
||||
## * the FILE WINS over the real environment, and the conflict is reported;
|
||||
## * an explicitly requested missing file raises (the caller then exits
|
||||
## nonzero), while a missing DEFAULT file is a silent no-op.
|
||||
|
||||
import std/[os, strutils]
|
||||
import "../../ModularBot_garage/src/env_dotenv"
|
||||
|
||||
var failures = 0
|
||||
var checks = 0
|
||||
|
||||
proc check(name: string, ok: bool) =
|
||||
inc checks
|
||||
if ok: echo "PASS: ", name
|
||||
else: echo "FAIL: ", name; inc failures
|
||||
|
||||
let tmpDir = getTempDir() / "test_env_dotenv_guard"
|
||||
createDir(tmpDir)
|
||||
|
||||
proc tmpPath(name: string): string = tmpDir / name
|
||||
|
||||
proc writeTmp(name, content: string): string =
|
||||
result = tmpPath(name)
|
||||
writeFile(result, content)
|
||||
|
||||
# ── 1. the parser ────────────────────────────────────────────────────────────
|
||||
|
||||
proc testParser() =
|
||||
let entries = parseEnvFileContent("""# a comment, ignored
|
||||
|
||||
TR_MOVEMENT=tfil
|
||||
export TR_RACK_TMHORIZON=both
|
||||
QUOTED="hello world"
|
||||
SINGLE='x y'
|
||||
EMPTY=
|
||||
SPACED = trimmed
|
||||
""", "sample.env")
|
||||
check "comment + blank lines are skipped (6 entries)", entries.len == 6
|
||||
if entries.len == 6:
|
||||
check "plain KEY=VALUE", entries[0].key == "TR_MOVEMENT" and entries[0].value == "tfil"
|
||||
check "leading `export ` is tolerated",
|
||||
entries[1].key == "TR_RACK_TMHORIZON" and entries[1].value == "both"
|
||||
check "double quotes are stripped",
|
||||
entries[2].key == "QUOTED" and entries[2].value == "hello world"
|
||||
check "single quotes are stripped",
|
||||
entries[3].key == "SINGLE" and entries[3].value == "x y"
|
||||
check "an empty value is allowed",
|
||||
entries[4].key == "EMPTY" and entries[4].value == ""
|
||||
check "whitespace around key and value is trimmed",
|
||||
entries[5].key == "SPACED" and entries[5].value == "trimmed"
|
||||
|
||||
let crlf = parseEnvFileContent("A=1\r\nB=two\r\n", "crlf.env")
|
||||
check "CRLF line endings are stripped from the value",
|
||||
crlf.len == 2 and crlf[0].value == "1" and crlf[1].value == "two"
|
||||
|
||||
var raised = false
|
||||
try:
|
||||
discard parseEnvFileContent("this line has no equals sign\n", "bad.env")
|
||||
except ValueError as e:
|
||||
raised = true
|
||||
check "malformed line error names the file", "bad.env" in e.msg
|
||||
check "malformed line error names the line number", ":1:" in e.msg
|
||||
check "malformed line RAISES (never silently ignored)", raised
|
||||
|
||||
var emptyKeyRaised = false
|
||||
try:
|
||||
discard parseEnvFileContent("=value\n", "bad2.env")
|
||||
except ValueError:
|
||||
emptyKeyRaised = true
|
||||
check "an empty key raises", emptyKeyRaised
|
||||
|
||||
# ── 2. resolution precedence ─────────────────────────────────────────────────
|
||||
|
||||
proc testResolution() =
|
||||
let flagChoice = chooseEnvFile("/x/flag.env", "/x/tr.env",
|
||||
"/cwd/.env", "/exe/.env")
|
||||
check "flag beats TR_ENV_FILE and the defaults",
|
||||
flagChoice.source == "flag" and flagChoice.path == "/x/flag.env" and
|
||||
flagChoice.explicit
|
||||
|
||||
let envChoice = chooseEnvFile("", "/x/tr.env", "/cwd/.env", "/exe/.env")
|
||||
check "TR_ENV_FILE beats the default paths",
|
||||
envChoice.source == EnvFileEnvVar and envChoice.path == "/x/tr.env" and
|
||||
envChoice.explicit
|
||||
|
||||
let cwdFile = writeTmp("cwd.env", "A=1\n")
|
||||
let exeFile = writeTmp("exe.env", "A=2\n")
|
||||
let cwdChoice = chooseEnvFile("", "", cwdFile, exeFile)
|
||||
check "./.env (cwd) is chosen when it exists and is not explicit",
|
||||
cwdChoice.source == "cwd" and cwdChoice.path == cwdFile and
|
||||
not cwdChoice.explicit
|
||||
|
||||
let exeChoice = chooseEnvFile("", "", tmpPath("missing_cwd.env"), exeFile)
|
||||
check ".env next to the executable is the fallback",
|
||||
exeChoice.source == "exe" and exeChoice.path == exeFile
|
||||
|
||||
let noneChoice = chooseEnvFile("", "", tmpPath("missing_cwd.env"),
|
||||
tmpPath("missing_exe.env"))
|
||||
check "no candidate exists -> none (silent)",
|
||||
noneChoice.path == "" and noneChoice.source == "none" and
|
||||
not noneChoice.explicit
|
||||
|
||||
check "--env-file <path> is parsed",
|
||||
flagEnvFilePath(@["--server", "x", "--env-file", "/a/b.env"]) == "/a/b.env"
|
||||
check "--env-file=<path> is parsed",
|
||||
flagEnvFilePath(@["--env-file=/a/b.env"]) == "/a/b.env"
|
||||
check "no --env-file yields empty", flagEnvFilePath(@["--server", "x"]) == ""
|
||||
var raised = false
|
||||
try:
|
||||
discard flagEnvFilePath(@["--env-file"])
|
||||
except EnvFileError:
|
||||
raised = true
|
||||
check "--env-file with no argument raises", raised
|
||||
|
||||
# ── 3. the file wins over the real environment ───────────────────────────────
|
||||
|
||||
proc testFileWins() =
|
||||
let f = writeTmp("wins.env", "TR_TEST_J126=from-file\nTR_TEST_SAME_J126=same\n")
|
||||
putEnv("TR_TEST_J126", "from-shell")
|
||||
putEnv("TR_TEST_SAME_J126", "same")
|
||||
let conflicts = applyEnvFile(f)
|
||||
check "the file value WINS over the exported value",
|
||||
getEnv("TR_TEST_J126") == "from-file"
|
||||
check "the conflict is reported with key, file value and shell value",
|
||||
conflicts.len == 1 and conflicts[0].key == "TR_TEST_J126" and
|
||||
conflicts[0].fileValue == "from-file" and
|
||||
conflicts[0].shellValue == "from-shell"
|
||||
check "an identical value is NOT a conflict",
|
||||
conflicts.len == 1 # TR_TEST_SAME_J126 is absent from conflicts
|
||||
check "keys from the file are tracked for the boot report",
|
||||
isFromEnvFile("TR_TEST_J126") and isFromEnvFile("TR_TEST_SAME_J126")
|
||||
delEnv("TR_TEST_J126")
|
||||
delEnv("TR_TEST_SAME_J126")
|
||||
|
||||
# ── 4. missing explicit file vs missing default file ─────────────────────────
|
||||
|
||||
proc testMissingFiles() =
|
||||
var raised = false
|
||||
try:
|
||||
discard loadEnvFile(EnvFileChoice(path: tmpPath("nope.env"),
|
||||
source: "flag", explicit: true))
|
||||
except EnvFileError as e:
|
||||
raised = true
|
||||
check "missing explicit file error names the path", "nope.env" in e.msg
|
||||
check "missing explicit file RAISES (caller exits nonzero)", raised
|
||||
|
||||
var quiet = true
|
||||
var n = 0
|
||||
try:
|
||||
n = loadEnvFile(EnvFileChoice(path: tmpPath("missing-default.env"),
|
||||
source: "cwd", explicit: false)).len
|
||||
except CatchableError:
|
||||
quiet = false
|
||||
check "missing default file is a silent no-op", quiet and n == 0
|
||||
|
||||
check "no file at all is a silent no-op",
|
||||
loadEnvFile(EnvFileChoice(path: "", source: "none", explicit: false)).len == 0
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
testParser()
|
||||
testResolution()
|
||||
testFileWins()
|
||||
testMissingFiles()
|
||||
|
||||
echo "\n", checks, " checks, ", failures, " failure(s)"
|
||||
if failures > 0:
|
||||
echo checks, " check(s) run, ", failures, " FAILED"
|
||||
quit(1)
|
||||
echo "All env-dotenv checks passed."
|
||||
@@ -1,5 +1,29 @@
|
||||
# Environment variable reference
|
||||
|
||||
## Putting your settings in a .env file
|
||||
|
||||
Instead of typing `export ...` in a shell, put your settings in a file. The bot
|
||||
reads them for you. This is easier to keep tidy, and no stale shell variable can
|
||||
surprise you.
|
||||
|
||||
1. In the bot folder, copy `.env.example` to `.env`.
|
||||
2. Open `.env` and set the knobs you want. One `KEY=VALUE` per line. Lines
|
||||
starting with `#` are comments.
|
||||
3. Start the bot from the bot folder (`./ModularBot`). It picks up `.env`
|
||||
automatically.
|
||||
4. To use a different file, pass it on the command line:
|
||||
`./out/ModularBot --env-file /path/to/my.env`.
|
||||
5. If you ask for a file that does not exist, the bot stops with an error. A
|
||||
missing default `.env` is fine and its absence is silent.
|
||||
|
||||
**The file wins over the shell.** If the same name is set in both places with a
|
||||
different value, the file value is used, and the bot prints one line telling you
|
||||
which shell value was overridden. Start the report check with:
|
||||
`grep '^\[env\]' /tmp/modularbot_stdout.log`. Values that came from the file are
|
||||
labelled `(source: .env)`.
|
||||
|
||||
---
|
||||
|
||||
Every knob the bot reads. **Most are read once per process, at module init; a few
|
||||
are read lazily on first use** (`TR_TMHORIZON_SHIFT`/`_BIG_MULT`/`_LOG`/
|
||||
`_RESET_ON_TARGET` in `ensureConfig`, `TR_PATTERN_RAD_*` inside `predict`). Either
|
||||
|
||||
Reference in New Issue
Block a user