Newer
Older
dub_jkp / source / configy / Test.d
/*******************************************************************************
    Contains all the tests for this library.

    Copyright:
        Copyright (c) 2019-2022 BOSAGORA Foundation
        All rights reserved.

    License:
        MIT License. See LICENSE for details.

*******************************************************************************/

module configy.Test;

import configy.Attributes;
import configy.Exceptions;
import configy.Read;
import configy.Utils;

import dyaml.node;

import std.format;

import core.time;

/// Basic usage tests
unittest
{
    static struct Address
    {
        string address;
        string city;
        bool accessible;
    }

    static struct Nested
    {
        Address address;
    }

    static struct Config
    {
        bool enabled = true;

        string name = "Jessie";
        int age = 42;
        double ratio = 24.42;

        Address address = { address: "Yeoksam-dong", city: "Seoul", accessible: true };

        Nested nested = { address: { address: "Gangnam-gu", city: "Also Seoul", accessible: false } };
    }

    auto c1 = parseConfigString!Config("enabled: false", "/dev/null");
    assert(c1.name == "Jessie");
    assert(c1.age == 42);
    assert(c1.ratio == 24.42);

    assert(c1.address.address == "Yeoksam-dong");
    assert(c1.address.city == "Seoul");
    assert(c1.address.accessible);

    assert(c1.nested.address.address == "Gangnam-gu");
    assert(c1.nested.address.city == "Also Seoul");
    assert(!c1.nested.address.accessible);
}

// Tests for SetInfo
unittest
{
    static struct Address
    {
        string address;
        string city;
        bool accessible;
    }

    static struct Config
    {
        SetInfo!int value;
        SetInfo!int answer = 42;
        SetInfo!string name = SetInfo!string("Lorene", false);

        SetInfo!Address address;
    }

    auto c1 = parseConfigString!Config("value: 24", "/dev/null");
    assert(c1.value == 24);
    assert(c1.value.set);

    assert(c1.answer.set);
    assert(c1.answer == 42);

    assert(!c1.name.set);
    assert(c1.name == "Lorene");

    assert(!c1.address.set);

    auto c2 = parseConfigString!Config(`
name: Lorene
address:
  address: Somewhere
  city:    Over the rainbow
`, "/dev/null");

    assert(!c2.value.set);
    assert(c2.name == "Lorene");
    assert(c2.name.set);
    assert(c2.address.set);
    assert(c2.address.address == "Somewhere");
    assert(c2.address.city == "Over the rainbow");
}

unittest
{
    static struct Nested { core.time.Duration timeout; }
    static struct Config { Nested node; }
    try
    {
        auto result = parseConfigString!Config("node:\n  timeout:", "/dev/null");
        assert(0);
    }
    catch (Exception exc)
    {
        assert(exc.toString() == "/dev/null(1:10): node.timeout: Field is of type scalar, " ~
               "but expected a mapping with at least one of: weeks, days, hours, minutes, " ~
               "seconds, msecs, usecs, hnsecs, nsecs");
    }
}

unittest
{
    static struct Config { string required; }
    try
        auto result = parseConfigString!Config("value: 24", "/dev/null");
    catch (ConfigException e)
    {
        assert(format("%s", e) ==
               "/dev/null(0:0): value: Key is not a valid member of this section. There are 1 valid keys: required");
        assert(format("%S", e) ==
               format("%s/dev/null%s(%s0%s:%s0%s): %svalue%s: Key is not a valid member of this section. " ~
                      "There are %s1%s valid keys: %srequired%s", Yellow, Reset, Cyan, Reset, Cyan, Reset,
                      Yellow, Reset, Yellow, Reset, Green, Reset));
    }
}

// Test for various type errors
unittest
{
    static struct Mapping
    {
        string value;
    }

    static struct Config
    {
        @Optional Mapping map;
        @Optional Mapping[] array;
        int scalar;
    }

    try
    {
        auto result = parseConfigString!Config("map: Hello World", "/dev/null");
        assert(0);
    }
    catch (ConfigException exc)
    {
        assert(exc.toString() == "/dev/null(0:5): map: Expected to be of type mapping (object), but is a scalar");
    }

    try
    {
        auto result = parseConfigString!Config("map:\n  - Hello\n  - World", "/dev/null");
        assert(0);
    }
    catch (ConfigException exc)
    {
        assert(exc.toString() == "/dev/null(1:2): map: Expected to be of type mapping (object), but is a sequence");
    }

    try
    {
        auto result = parseConfigString!Config("scalar:\n  - Hello\n  - World", "/dev/null");
        assert(0);
    }
    catch (ConfigException exc)
    {
        assert(exc.toString() == "/dev/null(1:2): scalar: Expected to be of type scalar (value), but is a sequence");
    }

    try
    {
        auto result = parseConfigString!Config("scalar:\n  hello:\n    World", "/dev/null");
        assert(0);
    }
    catch (ConfigException exc)
    {
        assert(exc.toString() == "/dev/null(1:2): scalar: Expected to be of type scalar (value), but is a mapping");
    }
}

// Test for strict mode
unittest
{
    static struct Config
    {
        string value;
    }

    try
    {
        auto result = parseConfigString!Config("valeu: This is a typo", "/dev/null");
        assert(0);
    }
    catch (ConfigException exc)
    {
        assert(exc.toString() == "/dev/null(0:0): valeu: Key is not a valid member of this section. There are 1 valid keys: value");
    }
}

// Test for required key
unittest
{
    static struct Nested
    {
        string required;
        string optional = "Default";
    }

    static struct Config
    {
        Nested inner;
    }

    try
    {
        auto result = parseConfigString!Config("inner:\n  optional: Not the default value", "/dev/null");
        assert(0);
    }
    catch (ConfigException exc)
    {
        assert(exc.toString() == "/dev/null(1:2): inner.required: Required key was not found in configuration or command line arguments");
    }
}

// Testing 'validate()' on nested structures
unittest
{
    __gshared int validateCalls0 = 0;
    __gshared int validateCalls1 = 1;
    __gshared int validateCalls2 = 2;

    static struct SecondLayer
    {
        string value = "default";

        public void validate () const
        {
            validateCalls2++;
        }
    }

    static struct FirstLayer
    {
        bool enabled = true;
        SecondLayer ltwo;

        public void validate () const
        {
            validateCalls1++;
        }
    }

    static struct Config
    {
        FirstLayer lone;

        public void validate () const
        {
            validateCalls0++;
        }
    }

    auto r1 = parseConfigString!Config("lone:\n  ltwo:\n    value: Something\n", "/dev/null");

    assert(r1.lone.ltwo.value == "Something");
    // `validateCalls` are given different value to avoid false-positive
    // if they are set to 0 / mixed up
    assert(validateCalls0 == 1);
    assert(validateCalls1 == 2);
    assert(validateCalls2 == 3);

    auto r2 = parseConfigString!Config("lone:\n  enabled: false\n", "/dev/null");
    assert(validateCalls0 == 2); // + 1
    assert(validateCalls1 == 2); // Other are disabled
    assert(validateCalls2 == 3);
}

// Test the throwing ctor / fromString
unittest
{
    static struct ThrowingFromString
    {
        public static ThrowingFromString fromString (scope const(char)[] value)
            @safe pure
        {
            throw new Exception("Some meaningful error message");
        }

        public int value;
    }

    static struct ThrowingCtor
    {
        public this (scope const(char)[] value)
            @safe pure
        {
            throw new Exception("Something went wrong... Obviously");
        }

        public int value;
    }

    static struct InnerConfig
    {
        public int value;
        @Optional ThrowingCtor ctor;
        @Optional ThrowingFromString fromString;

        @Converter!int(
            (Node value) {
                // We have to trick DMD a bit so that it infers an `int` return
                // type but doesn't emit a "Statement is not reachable" warning
                if (value is Node.init || value !is Node.init )
                    throw new Exception("You shall not pass");
                return 42;
            })
        @Optional int converter;
    }

    static struct Config
    {
        public InnerConfig config;
    }

    try
    {
        auto result = parseConfigString!Config("config:\n  value: 42\n  ctor: 42", "/dev/null");
        assert(0);
    }
    catch (ConfigException exc)
    {
        assert(exc.toString() == "/dev/null(2:8): config.ctor: Something went wrong... Obviously");
    }

    try
    {
        auto result = parseConfigString!Config("config:\n  value: 42\n  fromString: 42", "/dev/null");
        assert(0);
    }
    catch (ConfigException exc)
    {
        assert(exc.toString() == "/dev/null(2:14): config.fromString: Some meaningful error message");
    }

    try
    {
        auto result = parseConfigString!Config("config:\n  value: 42\n  converter: 42", "/dev/null");
        assert(0);
    }
    catch (ConfigException exc)
    {
        assert(exc.toString() == "/dev/null(2:13): config.converter: You shall not pass");
    }

    // We also need to test with arrays, to ensure they are correctly called
    static struct InnerArrayConfig
    {
        @Optional int value;
        @Optional ThrowingCtor ctor;
        @Optional ThrowingFromString fromString;
    }

    static struct ArrayConfig
    {
        public InnerArrayConfig[] configs;
    }

    try
    {
        auto result = parseConfigString!ArrayConfig("configs:\n  - ctor: something", "/dev/null");
        assert(0);
    }
    catch (ConfigException exc)
    {
        assert(exc.toString() == "/dev/null(1:10): configs[0].ctor: Something went wrong... Obviously");
    }

    try
    {
        auto result = parseConfigString!ArrayConfig(
            "configs:\n  - value: 42\n  - fromString: something", "/dev/null");
        assert(0);
    }
    catch (ConfigException exc)
    {
        assert(exc.toString() == "/dev/null(2:16): configs[1].fromString: Some meaningful error message");
    }
}

// Test duplicate fields detection
unittest
{
    static struct Config
    {
        @Name("shadow") int value;
        @Name("value")  int shadow;
    }

    auto result = parseConfigString!Config("shadow: 42\nvalue: 84\n", "/dev/null");
    assert(result.value  == 42);
    assert(result.shadow == 84);

    static struct BadConfig
    {
        int value;
        @Name("value") int something;
    }

    // Cannot test the error message, so this is as good as it gets
    static assert(!is(typeof(() {
                    auto r = parseConfigString!BadConfig("shadow: 42\nvalue: 84\n", "/dev/null");
                })));
}

// Test a renamed `enabled` / `disabled`
unittest
{
    static struct ConfigA
    {
        @Name("enabled") bool shouldIStay;
        int value;
    }

    static struct ConfigB
    {
        @Name("disabled") bool orShouldIGo;
        int value;
    }

    {
        auto c = parseConfigString!ConfigA("enabled: true\nvalue: 42", "/dev/null");
        assert(c.shouldIStay == true);
        assert(c.value == 42);
    }

    {
        auto c = parseConfigString!ConfigB("disabled: false\nvalue: 42", "/dev/null");
        assert(c.orShouldIGo == false);
        assert(c.value == 42);
    }
}

// Test for 'mightBeOptional' & missing key
unittest
{
    static struct RequestLimit { size_t reqs = 100; }
    static struct Nested       { @Name("jay") int value; }
    static struct Config { @Name("chris") Nested value; RequestLimit limits; }

    auto r = parseConfigString!Config("chris:\n  jay: 42", "/dev/null");
    assert(r.limits.reqs == 100);

    try
    {
        auto _ = parseConfigString!Config("limits:\n  reqs: 42", "/dev/null");
    }
    catch (ConfigException exc)
    {
        assert(exc.toString() == "(0:0): chris.jay: Required key was not found in configuration or command line arguments");
    }
}

// Support for associative arrays
unittest
{
    static struct Nested
    {
        int[string] answers;
    }

    static struct Parent
    {
        Nested[string] questions;
        string[int] names;
    }

    auto c = parseConfigString!Parent(
`names:
  42: "Forty two"
  97: "Quatre vingt dix sept"
questions:
  first:
    answers:
      # Need to use quotes here otherwise it gets interpreted as
      # true / false, perhaps a dyaml issue ?
      'yes': 42
      'no':  24
  second:
    answers:
      maybe:  69
      whynot: 20
`, "/dev/null");

    assert(c.names == [42: "Forty two", 97: "Quatre vingt dix sept"]);
    assert(c.questions.length == 2);
    assert(c.questions["first"] == Nested(["yes": 42, "no": 24]));
    assert(c.questions["second"] == Nested(["maybe": 69, "whynot": 20]));
}

unittest
{
    static struct FlattenMe
    {
        int value;
        string name;
    }

    static struct Config
    {
        FlattenMe flat = FlattenMe(24, "Four twenty");
        alias flat this;

        FlattenMe not_flat;
    }

    auto c = parseConfigString!Config(
        "value: 42\nname: John\nnot_flat:\n  value: 69\n  name: Henry",
        "/dev/null");
    assert(c.flat.value == 42);
    assert(c.flat.name == "John");
    assert(c.not_flat.value == 69);
    assert(c.not_flat.name == "Henry");

    auto c2 = parseConfigString!Config(
        "not_flat:\n  value: 69\n  name: Henry", "/dev/null");
    assert(c2.flat.value == 24);
    assert(c2.flat.name == "Four twenty");

    static struct OptConfig
    {
        @Optional FlattenMe flat;
        alias flat this;

        int value;
    }
    auto c3 = parseConfigString!OptConfig("value: 69\n", "/dev/null");
    assert(c3.value == 69);
}

unittest
{
    static struct Config
    {
        @Name("names")
        string[] names_;

        size_t names () const scope @safe pure nothrow @nogc
        {
            return this.names_.length;
        }
    }

    auto c = parseConfigString!Config("names:\n  - John\n  - Luca\n", "/dev/null");
    assert(c.names_ == [ "John", "Luca" ]);
    assert(c.names == 2);
}

// Make sure unions don't compile
unittest
{
    static union MyUnion
    {
        string value;
        int number;
    }

    static struct Config
    {
        MyUnion hello;
    }

    static assert(!is(typeof(parseConfigString!Config("hello: world\n", "/dev/null"))));
    static assert(!is(typeof(parseConfigString!MyUnion("hello: world\n", "/dev/null"))));
}

// Test the `@Key` attribute
unittest
{
    static struct Interface
    {
        string name;
        string static_ip;
    }

    static struct Config
    {
        string profile;

        @Key("name")
        immutable(Interface)[] ifaces = [
            Interface("lo", "127.0.0.1"),
        ];
    }

    auto c = parseConfigString!Config(`profile: default
ifaces:
  eth0:
    static_ip: "192.168.1.42"
  lo:
    static_ip: "127.0.0.42"
`, "/dev/null");
    assert(c.ifaces.length == 2);
    assert(c.ifaces == [ Interface("eth0", "192.168.1.42"), Interface("lo", "127.0.0.42")]);
}