changed how tests are ran; updated dotenv to handle parsing fields into structs
This commit is contained in:
+8
-1
@@ -46,7 +46,14 @@ build-web: ## WebAssembly build -> build/web (needs emscripten)
|
|||||||
|
|
||||||
test: ## Run Odin unit tests
|
test: ## Run Odin unit tests
|
||||||
@mkdir -p lib/local
|
@mkdir -p lib/local
|
||||||
$(ODIN) test test $(ODIN_FLAGS)
|
@if ls test/*.odin >/dev/null 2>&1; then \
|
||||||
|
echo "== gui/test =="; \
|
||||||
|
$(ODIN) test test $(ODIN_FLAGS); \
|
||||||
|
fi
|
||||||
|
@for dir in $$(find lib/local -name '*_test.odin' -exec dirname {} \; | sort -u); do \
|
||||||
|
echo "== $$dir =="; \
|
||||||
|
$(ODIN) test "$$dir" $(ODIN_FLAGS); \
|
||||||
|
done
|
||||||
|
|
||||||
clean: ## Remove build artifacts
|
clean: ## Remove build artifacts
|
||||||
rm -rf $(BIN) build
|
rm -rf $(BIN) build
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
package dotenv
|
|
||||||
|
|
||||||
import "core:os"
|
|
||||||
import "core:strconv"
|
|
||||||
import "core:strings"
|
|
||||||
|
|
||||||
// parse parses dotenv-format source (KEY=VALUE lines) into a map allocated
|
|
||||||
// with allocator. Blank lines, lines starting with '#', and lines without a
|
|
||||||
// '=' are skipped. Keys and values are trimmed; values may be wrapped in
|
|
||||||
// double quotes. Precedence with the real process environment is handled by
|
|
||||||
// the get_* accessors (real env vars win over the file).
|
|
||||||
parse :: proc(src: string, allocator := context.allocator) -> map[string]string {
|
|
||||||
result := make(map[string]string, allocator)
|
|
||||||
|
|
||||||
it := src
|
|
||||||
for line in strings.split_lines_iterator(&it) {
|
|
||||||
tr := strings.trim_space(line)
|
|
||||||
if len(tr) == 0 || strings.has_prefix(tr, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
eq := strings.index_byte(tr, '=')
|
|
||||||
if eq < 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
key := strings.trim_space(tr[:eq])
|
|
||||||
if key == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
value := strings.trim_space(tr[eq + 1:])
|
|
||||||
if len(value) >= 2 && value[0] == '"' && value[len(value) - 1] == '"' {
|
|
||||||
value = value[1:len(value) - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// clone so the map outlives the source buffer (e.g. a freed file read)
|
|
||||||
result[strings.clone(key, allocator)] = strings.clone(value, allocator)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse_file reads a dotenv file from disk and parses it. It returns
|
|
||||||
// (nil, false) when the file cannot be read (e.g. it does not exist).
|
|
||||||
parse_file :: proc(filename: string, allocator := context.allocator) -> (map[string]string, bool) {
|
|
||||||
data, err := os.read_entire_file(filename, allocator)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
defer delete(data)
|
|
||||||
|
|
||||||
return parse(string(data), allocator), true
|
|
||||||
}
|
|
||||||
|
|
||||||
// destroy frees the cloned keys/values and the map itself. Use it to release
|
|
||||||
// a map returned by parse/parse_file (plain delete does not free the strings).
|
|
||||||
destroy :: proc(env: map[string]string) {
|
|
||||||
for key, value in env {
|
|
||||||
delete(key)
|
|
||||||
delete(value)
|
|
||||||
}
|
|
||||||
delete(env)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get_string returns the value for key from the real process environment if
|
|
||||||
// set, otherwise from the parsed dotenv map (or "" if neither has it).
|
|
||||||
get_string :: proc(env: map[string]string, key: string) -> string {
|
|
||||||
if value, found := os.lookup_env(key, context.temp_allocator); found {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return env[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
// get_bool resolves key and parses it as a boolean (true/false, 1/0,
|
|
||||||
// yes/no, on/off). Missing or unparsable values yield default.
|
|
||||||
get_bool :: proc(env: map[string]string, key: string, default := false) -> bool {
|
|
||||||
if v := get_string(env, key); v != "" {
|
|
||||||
if parsed, ok := strconv.parse_bool(v); ok {
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return default
|
|
||||||
}
|
|
||||||
|
|
||||||
// get_int resolves key and parses it as an integer. Missing or unparsable
|
|
||||||
// values yield default.
|
|
||||||
get_int :: proc(env: map[string]string, key: string, default := 0) -> int {
|
|
||||||
if v := get_string(env, key); v != "" {
|
|
||||||
if parsed, ok := strconv.parse_int(v); ok {
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return default
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
package dotenv
|
||||||
|
|
||||||
|
import "base:runtime"
|
||||||
|
import "core:os"
|
||||||
|
import "core:reflect"
|
||||||
|
import "core:strconv"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
// parse parses dotenv-format source (KEY=VALUE lines) into a map allocated
|
||||||
|
// with allocator. Blank lines, lines starting with '#', and lines without a
|
||||||
|
// '=' are skipped. Keys and values are trimmed; values may be wrapped in
|
||||||
|
// double quotes. Real process environment variables take precedence over the
|
||||||
|
// file. The returned map owns its keys/values; release it with destroy.
|
||||||
|
@(private)
|
||||||
|
parse :: proc(
|
||||||
|
src: string,
|
||||||
|
allocator := context.allocator,
|
||||||
|
) -> map[string]string {
|
||||||
|
result := make(map[string]string, allocator)
|
||||||
|
|
||||||
|
it := src
|
||||||
|
for line in strings.split_lines_iterator(&it) {
|
||||||
|
tr := strings.trim_space(line)
|
||||||
|
if len(tr) == 0 || strings.has_prefix(tr, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
eq := strings.index_byte(tr, '=')
|
||||||
|
if eq < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.trim_space(tr[:eq])
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
value := strings.trim_space(tr[eq + 1:])
|
||||||
|
if len(value) >= 2 && value[0] == '"' && value[len(value) - 1] == '"' {
|
||||||
|
value = value[1:len(value) - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// real process env vars win over the file
|
||||||
|
if override, found := os.lookup_env(key, context.temp_allocator); found {
|
||||||
|
value = override
|
||||||
|
}
|
||||||
|
|
||||||
|
// clone so the map outlives the source buffer (e.g. a freed file read)
|
||||||
|
result[strings.clone(key, allocator)] = strings.clone(value, allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse_file reads a dotenv file from disk and parses it into a map. It
|
||||||
|
// returns (nil, false) when the file cannot be read (e.g. it does not exist).
|
||||||
|
parse_file :: proc(
|
||||||
|
filename: string,
|
||||||
|
allocator := context.allocator,
|
||||||
|
) -> (
|
||||||
|
map[string]string,
|
||||||
|
bool,
|
||||||
|
) {
|
||||||
|
data, err := os.read_entire_file(filename, allocator)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
defer delete(data)
|
||||||
|
|
||||||
|
return parse(string(data), allocator), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// destroy frees the cloned keys/values and the map itself. Use it to release
|
||||||
|
// a map returned by parse/parse_file (plain delete does not free the strings).
|
||||||
|
destroy :: proc(env: map[string]string) {
|
||||||
|
for key, value in env {
|
||||||
|
delete(key)
|
||||||
|
delete(value)
|
||||||
|
}
|
||||||
|
delete(env)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode populates dest's fields from env, matching each field by name
|
||||||
|
// (case-insensitively, so API_URL maps onto api_url). Values are converted
|
||||||
|
// to the field's type: string is cloned as-is into allocator, integers are
|
||||||
|
// parsed with strconv.parse_int (decimal/hex/negative), booleans with
|
||||||
|
// strconv.parse_bool, and floats with strconv.parse_f64. Keys missing from
|
||||||
|
// env leave the field at its zero value. It returns false if a present value
|
||||||
|
// cannot be converted to the field's type.
|
||||||
|
decode :: proc(
|
||||||
|
env: map[string]string,
|
||||||
|
dest: ^$T,
|
||||||
|
allocator := context.allocator,
|
||||||
|
) -> bool {
|
||||||
|
ti := reflect.type_info_base(type_info_of(T))
|
||||||
|
fields, ok := ti.variant.(runtime.Type_Info_Struct)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
field_ptr := rawptr(dest)
|
||||||
|
for _, i in fields.names[:fields.field_count] {
|
||||||
|
name = fields.names[i]
|
||||||
|
value = ""
|
||||||
|
found := false
|
||||||
|
for key, v in env {
|
||||||
|
if key == name || strings.equal_fold(key, name) {
|
||||||
|
value, found = v, true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
field_ptr = rawptr(uintptr(dest) + fields.offsets[i])
|
||||||
|
field_ti := reflect.type_info_base(fields.types[i])
|
||||||
|
#partial switch variant in field_ti.variant {
|
||||||
|
case runtime.Type_Info_String:
|
||||||
|
(^string)(field_ptr)^ = strings.clone(value, allocator)
|
||||||
|
case runtime.Type_Info_Integer:
|
||||||
|
parsed, err := strconv.parse_int(value)
|
||||||
|
if !err {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch field_ti.size {
|
||||||
|
case 1:
|
||||||
|
(^i8)(field_ptr)^ = cast(i8)parsed
|
||||||
|
case 2:
|
||||||
|
(^i16)(field_ptr)^ = cast(i16)parsed
|
||||||
|
case 4:
|
||||||
|
(^i32)(field_ptr)^ = cast(i32)parsed
|
||||||
|
case 8:
|
||||||
|
(^i64)(field_ptr)^ = cast(i64)parsed
|
||||||
|
case:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case runtime.Type_Info_Boolean:
|
||||||
|
parsed, err := strconv.parse_bool(value)
|
||||||
|
if !err {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
(^bool)(field_ptr)^ = parsed
|
||||||
|
case runtime.Type_Info_Float:
|
||||||
|
parsed, err := strconv.parse_f64(value)
|
||||||
|
if !err {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch field_ti.size {
|
||||||
|
case 4:
|
||||||
|
(^f32)(field_ptr)^ = cast(f32)parsed
|
||||||
|
case 8:
|
||||||
|
(^f64)(field_ptr)^ = parsed
|
||||||
|
case:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case:
|
||||||
|
// unsupported field type (slices, pointers, ...) is left untouched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
package dotenv_tests
|
||||||
|
|
||||||
|
import "core:os"
|
||||||
|
import "core:strings"
|
||||||
|
import "core:testing"
|
||||||
|
import dotenv "lib:dotenv/src"
|
||||||
|
|
||||||
|
Test_Config :: struct {
|
||||||
|
api_url: string,
|
||||||
|
debug: bool,
|
||||||
|
port: int,
|
||||||
|
ratio: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// load_env writes src to a unique temp file and parses it via parse_file.
|
||||||
|
// The returned map owns its strings; callers must destroy it.
|
||||||
|
load_env :: proc(t: ^testing.T, src: string) -> map[string]string {
|
||||||
|
dir, err := os.make_directory_temp("", "dotenv_test_*", context.allocator)
|
||||||
|
testing.expect(t, err == nil, "expected temp dir to be created")
|
||||||
|
defer os.remove_all(dir)
|
||||||
|
defer delete(dir)
|
||||||
|
|
||||||
|
path := strings.concatenate({dir, "/.env"})
|
||||||
|
defer delete(path)
|
||||||
|
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
os.write_entire_file(path, src) == nil,
|
||||||
|
"expected file write to succeed",
|
||||||
|
)
|
||||||
|
|
||||||
|
env, ok := dotenv.parse_file(path)
|
||||||
|
testing.expect(t, ok, "expected parse_file to succeed")
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_parse_basic :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "API_URL=http://127.0.0.1:8080\nDEBUG=true\nPORT=8080\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
testing.expect(t, env["API_URL"] == "http://127.0.0.1:8080")
|
||||||
|
testing.expect(t, env["DEBUG"] == "true")
|
||||||
|
testing.expect(t, env["PORT"] == "8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_parse_ignores_comments_and_blank_lines :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "# leading comment\n\n \nFOO=bar \nBAZ = qux \n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
testing.expect(t, env["FOO"] == "bar", "value should be trimmed")
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
env["BAZ"] == "qux",
|
||||||
|
"key and value should be trimmed around '='",
|
||||||
|
)
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
"API_URL" not_in env,
|
||||||
|
"comment-only lines should not be parsed",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_parse_quoted_values :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "GREETING=\"hello world\"\nEMPTY=\"\"\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
env["GREETING"] == "hello world",
|
||||||
|
"quoted value with inner space",
|
||||||
|
)
|
||||||
|
testing.expect(t, env["EMPTY"] == "", "double-quoted empty value")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_parse_skips_lines_without_equals :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "not-an-assignment\nOK=yep\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
testing.expect(t, env["OK"] == "yep")
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
"not-an-assignment" not_in env,
|
||||||
|
"line without '=' should be skipped",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_parse_missing_file :: proc(t: ^testing.T) {
|
||||||
|
env, ok := dotenv.parse_file("/nonexistent/dotenv_test_does_not_exist.env")
|
||||||
|
testing.expect(t, !ok, "missing file should report failure")
|
||||||
|
testing.expect(t, env == nil, "missing file should return nil map")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_real_env_overrides_file :: proc(t: ^testing.T) {
|
||||||
|
testing.expect(t, os.set_env("DESI_EXPLORER_TEST_FOO", "from_env") == nil)
|
||||||
|
defer os.unset_env("DESI_EXPLORER_TEST_FOO")
|
||||||
|
|
||||||
|
env := load_env(t, "DESI_EXPLORER_TEST_FOO=from_file\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
env["DESI_EXPLORER_TEST_FOO"] == "from_env",
|
||||||
|
"real env var should win over file",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_decode_maps_fields_case_insensitively :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(
|
||||||
|
t,
|
||||||
|
"API_URL=http://127.0.0.1:8080\nDEBUG=true\nPORT=8080\nRATIO=0.5\n",
|
||||||
|
)
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
cfg := Test_Config{}
|
||||||
|
testing.expect(t, dotenv.decode(env, &cfg))
|
||||||
|
defer delete(cfg.api_url)
|
||||||
|
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
cfg.api_url == "http://127.0.0.1:8080",
|
||||||
|
"API_URL maps onto api_url",
|
||||||
|
)
|
||||||
|
testing.expect(t, cfg.debug == true, "DEBUG=true should decode to true")
|
||||||
|
testing.expect(t, cfg.port == 8080, "PORT=8080 should decode to int 8080")
|
||||||
|
testing.expect(t, cfg.ratio == 0.5, "RATIO=0.5 should decode to f64 0.5")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_decode_matches_exact_and_lowercase_keys :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "api_url=http://exact\nPort=9090\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
cfg := Test_Config{}
|
||||||
|
testing.expect(t, dotenv.decode(env, &cfg))
|
||||||
|
defer delete(cfg.api_url)
|
||||||
|
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
cfg.api_url == "http://exact",
|
||||||
|
"exact-case key should match",
|
||||||
|
)
|
||||||
|
testing.expect(t, cfg.port == 9090, "mixed-case key should match field")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_decode_missing_keys_leave_zero_values :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "UNRELATED=value\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
cfg := Test_Config{}
|
||||||
|
testing.expect(t, dotenv.decode(env, &cfg))
|
||||||
|
|
||||||
|
testing.expect(t, cfg.api_url == "")
|
||||||
|
testing.expect(t, !cfg.debug)
|
||||||
|
testing.expect(t, cfg.port == 0)
|
||||||
|
testing.expect(t, cfg.ratio == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_decode_unparsable_int_fails :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "PORT=oops\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
cfg := Test_Config{}
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
!dotenv.decode(env, &cfg),
|
||||||
|
"unparsable int should make decode fail",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_decode_unparsable_bool_fails :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "DEBUG=maybe\nAPI_URL=http://127.0.0.1:8080\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
cfg := Test_Config{}
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
!dotenv.decode(env, &cfg),
|
||||||
|
"unparsable bool should make decode fail",
|
||||||
|
)
|
||||||
|
defer delete(cfg.api_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_decode_hex_and_negative_ints :: proc(t: ^testing.T) {
|
||||||
|
env := load_env(t, "PORT=0x1F\n")
|
||||||
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
|
cfg := Test_Config{}
|
||||||
|
testing.expect(t, dotenv.decode(env, &cfg))
|
||||||
|
testing.expect(t, cfg.port == 31, "hex int should decode")
|
||||||
|
}
|
||||||
+12
-4
@@ -1,18 +1,26 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "lib:dotenv"
|
import "core:os"
|
||||||
|
import dotenv "lib:dotenv/src"
|
||||||
|
|
||||||
Config :: struct {
|
Config :: struct {
|
||||||
api_url: string,
|
api_url: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
get_config :: proc() -> (^Config, ^Error) {
|
get_config :: proc(env_file: ^string = nil) -> (^Config, ^Error) {
|
||||||
env, _ := dotenv.parse_file(".env", context.temp_allocator)
|
env, _ := dotenv.parse_file(".env", context.temp_allocator)
|
||||||
defer dotenv.destroy(env)
|
defer dotenv.destroy(env)
|
||||||
|
|
||||||
c := new(Config)
|
c := new(Config)
|
||||||
|
|
||||||
c.api_url = dotenv.get_string(env, "API_URL")
|
if env == nil {
|
||||||
|
c.api_url = os.get_env("API_URL", context.temp_allocator)
|
||||||
|
} else if !dotenv.decode(env, c) {
|
||||||
|
err := new(Error)
|
||||||
|
err.type = .Config
|
||||||
|
err.message = "failed to decode .env into Config"
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if err := validate_config(c); err != nil {
|
if err := validate_config(c); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -26,4 +34,4 @@ validate_config :: proc(c: ^Config) -> ^Error {
|
|||||||
if c.api_url == "" do err.type = .Config; err.message = "'API_URL' is required"
|
if c.api_url == "" do err.type = .Config; err.message = "'API_URL' is required"
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
package dotenv_tests
|
|
||||||
|
|
||||||
import "core:os"
|
|
||||||
import "core:testing"
|
|
||||||
import "lib:dotenv"
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_parse_basic :: proc(t: ^testing.T) {
|
|
||||||
env := dotenv.parse("API_URL=http://127.0.0.1:8080\nDEBUG=true\nPORT=8080\n")
|
|
||||||
defer dotenv.destroy(env)
|
|
||||||
|
|
||||||
testing.expect(t, env["API_URL"] == "http://127.0.0.1:8080")
|
|
||||||
testing.expect(t, env["DEBUG"] == "true")
|
|
||||||
testing.expect(t, env["PORT"] == "8080")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_parse_ignores_comments_and_blank_lines :: proc(t: ^testing.T) {
|
|
||||||
env := dotenv.parse("# leading comment\n\n \nFOO=bar \nBAZ = qux \n")
|
|
||||||
defer dotenv.destroy(env)
|
|
||||||
|
|
||||||
testing.expect(t, env["FOO"] == "bar", "value should be trimmed")
|
|
||||||
testing.expect(t, env["BAZ"] == "qux", "key and value should be trimmed around '='")
|
|
||||||
testing.expect(t, "API_URL" not_in env, "comment-only lines should not be parsed")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_parse_quoted_values :: proc(t: ^testing.T) {
|
|
||||||
env := dotenv.parse("GREETING=\"hello world\"\nEMPTY=\"\"\n")
|
|
||||||
defer dotenv.destroy(env)
|
|
||||||
|
|
||||||
testing.expect(t, env["GREETING"] == "hello world", "quoted value with inner space")
|
|
||||||
testing.expect(t, env["EMPTY"] == "", "double-quoted empty value")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_parse_skips_lines_without_equals :: proc(t: ^testing.T) {
|
|
||||||
env := dotenv.parse("not-an-assignment\nOK=yep\n")
|
|
||||||
defer dotenv.destroy(env)
|
|
||||||
|
|
||||||
testing.expect(t, env["OK"] == "yep")
|
|
||||||
testing.expect(t, "not-an-assignment" not_in env, "line without '=' should be skipped")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_get_string_falls_back_to_file :: proc(t: ^testing.T) {
|
|
||||||
env := dotenv.parse("FOO=from_file\n")
|
|
||||||
defer dotenv.destroy(env)
|
|
||||||
|
|
||||||
testing.expect(t, dotenv.get_string(env, "FOO") == "from_file", "missing env var should use file value")
|
|
||||||
testing.expect(t, dotenv.get_string(env, "MISSING") == "", "absent everywhere should be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_get_string_prefers_real_env :: proc(t: ^testing.T) {
|
|
||||||
env := dotenv.parse("FOO=from_file\n")
|
|
||||||
defer dotenv.destroy(env)
|
|
||||||
|
|
||||||
testing.expect(t, os.set_env("FOO", "from_env") == nil)
|
|
||||||
defer os.unset_env("FOO")
|
|
||||||
|
|
||||||
testing.expect(t, dotenv.get_string(env, "FOO") == "from_env", "real env var should win over file")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_get_bool :: proc(t: ^testing.T) {
|
|
||||||
env := dotenv.parse("A=true\nB=0\nC=FALSE\nD=garbage\n")
|
|
||||||
defer dotenv.destroy(env)
|
|
||||||
|
|
||||||
testing.expect(t, dotenv.get_bool(env, "A", false), "\"true\" should parse to true")
|
|
||||||
testing.expect(t, !dotenv.get_bool(env, "B", true), "\"0\" should parse to false")
|
|
||||||
testing.expect(t, !dotenv.get_bool(env, "C", true), "\"FALSE\" should parse to false")
|
|
||||||
testing.expect(t, dotenv.get_bool(env, "D", true), "unparsable value should fall back to default")
|
|
||||||
testing.expect(t, dotenv.get_bool(env, "MISSING", true), "missing key should fall back to default")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_get_int :: proc(t: ^testing.T) {
|
|
||||||
env := dotenv.parse("PORT=8080\nNEG=-42\nHEX=0x1F\nGARBAGE=abc\n")
|
|
||||||
defer dotenv.destroy(env)
|
|
||||||
|
|
||||||
testing.expect(t, dotenv.get_int(env, "PORT", -1) == 8080, "decimal int")
|
|
||||||
testing.expect(t, dotenv.get_int(env, "NEG", 0) == -42, "negative int")
|
|
||||||
testing.expect(t, dotenv.get_int(env, "HEX", 0) == 31, "hex int")
|
|
||||||
testing.expect(t, dotenv.get_int(env, "GARBAGE", 7) == 7, "unparsable value should fall back to default")
|
|
||||||
testing.expect(t, dotenv.get_int(env, "MISSING", 7) == 7, "missing key should fall back to default")
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user