blob: 9d487d4083e44f3476a9c308cf25181814a42790 [file]
/*
* Copyright © 2025 Google, Inc.
*
* This is part of HarfBuzz, a text shaping library.
*
* Permission is hereby granted, without written agreement and without
* license or royalty fees, to use, copy, modify, and distribute this
* software and its documentation for any purpose, provided that the
* above copyright notice and the following two paragraphs appear in
* all copies of this software.
*
* IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
* DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
* ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
* IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*
* THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
* ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
* PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
*
* Google Author(s): Garret Rieger
*/
#include "hb-result.hh"
// Track constructor and destructor counts to verify leak-freedom
static int live_instances = 0;
enum error_code_t {
ERR_A,
ERR_B,
ERR_C,
};
template <typename T>
using result = hb_result_t<T, error_code_t>;
struct resource_t
{
int id;
explicit resource_t (int id_) : id (id_) { live_instances++; }
resource_t (const resource_t& o) : id (o.id) { live_instances++; }
resource_t (resource_t&& o) : id (o.id) { o.id = -1; live_instances++; }
~resource_t () { live_instances--; }
resource_t& operator = (const resource_t& o)
{
id = o.id;
return *this;
}
resource_t& operator = (resource_t&& o)
{
id = o.id;
o.id = -1;
return *this;
}
bool operator == (const resource_t& o) const { return id == o.id; }
};
static result<int> returns_ok (int v)
{
return Ok(v);
}
static result<int> returns_err (error_code_t e)
{
return Err(e);
}
static result<int> try_callee (bool fail, int v, error_code_t e = ERR_A)
{
if (fail) return Err(e);
return Ok(v);
}
static result<void> try_callee_void (bool fail, error_code_t e)
{
if (fail) return e;
return Ok();
}
static result<int> try_caller (bool fail1, bool fail2)
{
TRY_ASSIGN (int index, try_callee (fail1, 10, ERR_A));
TRY_ASSIGN (int index2, try_callee (fail2, 20, ERR_B));
return Ok(index + index2);
}
static result<void> try_caller_void (bool fail)
{
TRY (try_callee_void (fail, ERR_C));
return Ok();
}
static result<float> try_caller_type_change (bool fail)
{
TRY_ASSIGN (int index, try_callee (fail, 100, ERR_A));
return Ok((float) index * 1.5f);
}
static result<int> try_assign_existing (bool fail)
{
int val = 1;
TRY_ASSIGN (val, try_callee (fail, 42, ERR_A));
return Ok(val);
}
static void test_ok_basic ()
{
result<int> r = returns_ok (42);
hb_always_assert (r.is_ok ());
hb_always_assert (!r.is_err ());
hb_always_assert ((bool) r);
hb_always_assert (r.value () == 42);
hb_always_assert (*r == 42);
hb_always_assert (r.value_or (0) == 42);
hb_always_assert (r.value_or_default () == 42);
hb_always_assert (r == Ok(42));
hb_always_assert (Ok(42) == r);
}
static void test_err_basic ()
{
result<int> r = returns_err (ERR_A);
hb_always_assert (!r.is_ok ());
hb_always_assert (r.is_err ());
hb_always_assert (!r);
hb_always_assert (r.error () == ERR_A);
hb_always_assert (r.value_or (99) == 99);
hb_always_assert (r.value_or_default () == 0);
hb_always_assert (r == Err(ERR_A));
hb_always_assert (Err(ERR_A) == r);
hb_always_assert (r != Err(ERR_B));
hb_always_assert (Err(ERR_B) != r);
}
static void test_explicit_ok_err ()
{
result<unsigned> r1 = Ok(100u);
hb_always_assert (r1.is_ok ());
hb_always_assert (r1.value () == 100u);
result<unsigned> r2 = Err(ERR_A);
hb_always_assert (r2.is_err ());
hb_always_assert (r2.error () == ERR_A);
}
static void test_pointers ()
{
int x = 123;
result<int*> r = &x;
hb_always_assert (r.is_ok ());
hb_always_assert (*r == &x);
hb_always_assert (**r == 123);
**r = 456;
hb_always_assert (x == 456);
}
static void test_void ()
{
result<void> r1 = Ok();
hb_always_assert (r1.is_ok ());
hb_always_assert (!r1.is_err ());
hb_always_assert ((bool) r1);
hb_always_assert (r1 == Ok());
hb_always_assert (r1 != Err(ERR_A));
hb_always_assert (Ok() == r1);
hb_always_assert (Err(ERR_A) != r1);
result<void> r2 = Err(ERR_A);
hb_always_assert (!r2.is_ok ());
hb_always_assert (r2.is_err ());
hb_always_assert (!r2);
hb_always_assert (r2.error () == ERR_A);
hb_always_assert (r2 == Err(ERR_A));
hb_always_assert (r2 != Err(ERR_B));
hb_always_assert (r2 != Ok());
hb_always_assert (Err(ERR_A) == r2);
hb_always_assert (Err(ERR_B) != r2);
hb_always_assert (Ok() != r2);
}
static void test_resource_cleanup ()
{
hb_always_assert (live_instances == 0);
{
result<resource_t> r = resource_t (1);
hb_always_assert (live_instances == 1);
hb_always_assert (r.value ().id == 1);
}
hb_always_assert (live_instances == 0);
{
result<resource_t> r1 = resource_t (2);
hb_always_assert (live_instances == 1);
result<resource_t> r2 = r1;
hb_always_assert (live_instances == 2);
result<resource_t> r3 = std::move (r1);
hb_always_assert (live_instances == 3);
}
hb_always_assert (live_instances == 0);
{
result<resource_t> r = resource_t (3);
hb_always_assert (live_instances == 1);
r = Err(ERR_A);
hb_always_assert (live_instances == 0);
hb_always_assert (r.is_err ());
}
hb_always_assert (live_instances == 0);
}
static void test_try_macro ()
{
// Test success path
result<int> r_success = try_caller (false, false);
hb_always_assert (r_success.is_ok ());
hb_always_assert (r_success.value () == 30);
// Test first failure propagates
result<int> r_fail1 = try_caller (true, false);
hb_always_assert (r_fail1.is_err ());
hb_always_assert (r_fail1.error () == ERR_A);
// Test second failure propagates
result<int> r_fail2 = try_caller (false, true);
hb_always_assert (r_fail2.is_err ());
hb_always_assert (r_fail2.error () == ERR_B);
// Test TRY in void function
result<void> v_ok = try_caller_void (false);
hb_always_assert (v_ok.is_ok ());
result<void> v_err = try_caller_void (true);
hb_always_assert (v_err.is_err ());
hb_always_assert (v_err.error () == ERR_C);
// Test TRY with different return type
result<float> f_ok = try_caller_type_change (false);
hb_always_assert (f_ok.is_ok ());
hb_always_assert (f_ok.value () == 150.0f);
result<float> f_err = try_caller_type_change (true);
hb_always_assert (f_err.is_err ());
hb_always_assert (f_err.error () == ERR_A);
// Test TRY_ASSIGN into existing variable
result<int> a_ok = try_assign_existing (false);
hb_always_assert (a_ok.is_ok ());
hb_always_assert (a_ok.value () == 42);
result<int> a_err = try_assign_existing (true);
hb_always_assert (a_err.is_err ());
hb_always_assert (a_err.error () == ERR_A);
}
template <typename T, typename U, typename = void>
struct is_equality_comparable : std::false_type {};
template <typename T, typename U>
struct is_equality_comparable<T, U, hb_void_t<decltype (hb_declval (T) == hb_declval (U))>> : std::true_type {};
static void test_same_or_convertible_types ()
{
// Comparisons between result and raw types are disallowed
static_assert (!is_equality_comparable<result<int>, int>::value, "");
static_assert (!is_equality_comparable<result<int>, int64_t>::value, "");
static_assert (!is_equality_comparable<int, result<int>>::value, "");
static_assert (!is_equality_comparable<result<int>, error_code_t>::value, "");
static_assert (!is_equality_comparable<error_code_t, result<int>>::value, "");
static_assert (!is_equality_comparable<result<void>, error_code_t>::value, "");
static_assert (is_equality_comparable<result<int>, result<int>>::value, "");
static_assert (is_equality_comparable<result<int>, hb_result_ok_t<int>>::value, "");
static_assert (is_equality_comparable<result<int>, hb_result_err_t<error_code_t>>::value, "");
// Types convertible into each other cannot be directly constructed without Ok/Err
static_assert (!std::is_constructible<hb_result_t<int64_t, int32_t>, int>::value, "");
static_assert (!std::is_constructible<hb_result_t<int64_t, int32_t>, int64_t>::value, "");
static_assert (!std::is_constructible<hb_result_t<int64_t, int32_t>, int32_t>::value, "");
static_assert (std::is_constructible<hb_result_t<int64_t, int32_t>, hb_result_ok_t<int>>::value, "");
static_assert (std::is_constructible<hb_result_t<int64_t, int32_t>, hb_result_err_t<int>>::value, "");
// Same types cannot be directly constructed without Ok/Err
static_assert (!std::is_constructible<hb_result_t<int, int>, int>::value, "");
static_assert (std::is_constructible<hb_result_t<int, int>, hb_result_ok_t<int>>::value, "");
static_assert (std::is_constructible<hb_result_t<int, int>, hb_result_err_t<int>>::value, "");
// Types convertible into each other cannot be directly constructed
static_assert (!std::is_constructible<hb_result_t<int, error_code_t>, int>::value, "");
static_assert (!std::is_constructible<hb_result_t<int, error_code_t>, error_code_t>::value, "");
// Types not convertible into each other can be directly constructed
static_assert (std::is_constructible<hb_result_t<resource_t, error_code_t>, resource_t>::value, "");
static_assert (std::is_constructible<hb_result_t<resource_t, error_code_t>, error_code_t>::value, "");
hb_result_t<int64_t, int32_t> r_ok = Ok(1);
hb_always_assert (r_ok.is_ok ());
hb_always_assert (r_ok.value () == 1);
hb_result_t<int64_t, int32_t> r_err = Err(2);
hb_always_assert (r_err.is_err ());
hb_always_assert (r_err.error () == 2);
hb_result_t<int, int> same_ok = Ok(42);
hb_always_assert (same_ok.is_ok ());
hb_always_assert (same_ok.value () == 42);
hb_result_t<int, int> same_err = Err(99);
hb_always_assert (same_err.is_err ());
hb_always_assert (same_err.error () == 99);
}
struct any_value_t
{
template <typename X>
any_value_t (X&&) {}
};
static void test_error_interception() {
// Checks that U&& constructor does not intercept mismatched error tags as a success value.
static_assert (!std::is_constructible<hb_result_t<any_value_t, error_code_t>, hb_result_err_t<const char*>>::value, "");
hb_result_t<any_value_t, error_code_t> r = Err(ERR_A);
hb_always_assert (r.is_err ());
hb_result_t<any_value_t, error_code_t> r_ok = Ok(123);
hb_always_assert (r_ok.is_ok ());
}
struct move_only_err_t
{
int code;
explicit move_only_err_t (int c) : code (c) {}
move_only_err_t (const move_only_err_t&) = delete;
move_only_err_t (move_only_err_t&& o) : code (o.code) { o.code = -1; }
move_only_err_t& operator = (const move_only_err_t&) = delete;
move_only_err_t& operator = (move_only_err_t&& o) { code = o.code; o.code = -1; return *this; }
~move_only_err_t () = default;
bool operator == (const move_only_err_t& o) const { return code == o.code; }
};
struct move_only_val_t
{
int val;
explicit move_only_val_t (int v) : val (v) {}
move_only_val_t (const move_only_val_t&) = delete;
move_only_val_t (move_only_val_t&& o) : val (o.val) { o.val = -1; }
move_only_val_t& operator = (const move_only_val_t&) = delete;
move_only_val_t& operator = (move_only_val_t&& o) { val = o.val; o.val = -1; return *this; }
~move_only_val_t () = default;
bool operator == (const move_only_val_t& o) const { return val == o.val; }
};
static void test_move_only_types ()
{
// Test hb_result_t<T, move_only_err_t> with Err(...)
{
hb_result_t<int, move_only_err_t> r = Err(move_only_err_t (123));
hb_always_assert (r.is_err ());
hb_always_assert (r.error ().code == 123);
move_only_err_t err = std::move (r).error ();
hb_always_assert (err.code == 123);
}
// Test direct construction from move_only_err_t
{
hb_result_t<int, move_only_err_t> r = move_only_err_t (456);
hb_always_assert (r.is_err ());
hb_always_assert (r.error ().code == 456);
}
// Test hb_result_t<void, move_only_err_t> with Err(...) and direct construction
{
hb_result_t<void, move_only_err_t> v1 = Err(move_only_err_t (789));
hb_always_assert (v1.is_err ());
hb_always_assert (v1.error ().code == 789);
hb_result_t<void, move_only_err_t> v2 = move_only_err_t (999);
hb_always_assert (v2.is_err ());
hb_always_assert (v2.error ().code == 999);
}
// Test value_or with move-only value types
{
hb_result_t<move_only_val_t, error_code_t> r_ok = Ok(move_only_val_t (42));
move_only_val_t v = std::move (r_ok).value_or (move_only_val_t (0));
hb_always_assert (v.val == 42);
hb_result_t<move_only_val_t, error_code_t> r_err = Err(ERR_A);
move_only_val_t v_fallback = std::move (r_err).value_or (move_only_val_t (99));
hb_always_assert (v_fallback.val == 99);
}
}
struct copy_move_counter_t
{
static int copy_count;
static int move_count;
static void reset ()
{
copy_count = 0;
move_count = 0;
}
int val;
bool in_error() const {
return val == -1;
}
explicit copy_move_counter_t (int v) : val (v) {}
copy_move_counter_t (const copy_move_counter_t& o) : val (o.val) { copy_count++; }
copy_move_counter_t (copy_move_counter_t&& o) : val (o.val) { o.val = -1; move_count++; }
~copy_move_counter_t () = default;
};
int copy_move_counter_t::copy_count = 0;
int copy_move_counter_t::move_count = 0;
static result<copy_move_counter_t> return_tracked_from_local ()
{
copy_move_counter_t out (42);
return out;
}
static result<copy_move_counter_t> return_tracked_from_local_with_tag ()
{
copy_move_counter_t out (42);
return Ok(out);
}
static hb_result_t<int, copy_move_counter_t> return_tracked_error_from_local ()
{
copy_move_counter_t out (42);
return out;
}
static hb_result_t<int, copy_move_counter_t> return_tracked_error_from_local_with_tag ()
{
copy_move_counter_t out (42);
return Err(out);
}
static result<move_only_val_t> return_move_only_from_local ()
{
move_only_val_t out (42);
return out;
}
static void test_return_moves_from_local ()
{
// Verify 0 copies and only moves occur when returning local value directly
{
copy_move_counter_t::reset ();
result<copy_move_counter_t> r = return_tracked_from_local ();
hb_always_assert (r.is_ok ());
hb_always_assert (r.value ().val == 42);
hb_always_assert (copy_move_counter_t::copy_count == 0);
hb_always_assert (copy_move_counter_t::move_count >= 1);
}
// Verify 0 copies and only moves occur when returning local value directly
{
copy_move_counter_t::reset ();
result<copy_move_counter_t> r = return_tracked_from_local_with_tag ();
hb_always_assert (r.is_ok ());
hb_always_assert (r.value ().val == 42);
hb_always_assert (copy_move_counter_t::copy_count == 0);
hb_always_assert (copy_move_counter_t::move_count >= 1);
}
// Verify 0 copies and only moves occur when returning local error directly
{
copy_move_counter_t::reset ();
hb_result_t<int, copy_move_counter_t> r = return_tracked_error_from_local ();
hb_always_assert (r.is_err ());
hb_always_assert (r.error().val == 42);
hb_always_assert (copy_move_counter_t::copy_count == 0);
hb_always_assert (copy_move_counter_t::move_count >= 1);
}
// Verify 0 copies and only moves occur when returning local error directly
{
copy_move_counter_t::reset ();
hb_result_t<int, copy_move_counter_t> r = return_tracked_error_from_local_with_tag ();
hb_always_assert (r.is_err ());
hb_always_assert (r.error().val == 42);
hb_always_assert (copy_move_counter_t::copy_count == 0);
hb_always_assert (copy_move_counter_t::move_count >= 1);
}
// Test returning move-only type (statically proves no copy is attempted)
{
result<move_only_val_t> r = return_move_only_from_local ();
hb_always_assert (r.is_ok ());
hb_always_assert (r.value ().val == 42);
}
}
struct in_error_t {
bool successful;
in_error_t(bool s) : successful(s) {}
bool in_error() const {
return !successful;
}
};
static void test_from () {
in_error_t has_error(false);
in_error_t no_error(true);
result<void> r = result<void>::from(has_error, ERR_B);
hb_always_assert(r.is_err());
hb_always_assert(r.error() == ERR_B);
r = result<void>::from(no_error, ERR_B);
hb_always_assert(!r.is_err());
}
struct default_constructible_t
{
int val;
default_constructible_t () : val (123) {}
explicit default_constructible_t (int v) : val (v) {}
};
struct move_only_default_t
{
int val;
move_only_default_t () : val (77) {}
explicit move_only_default_t (int v) : val (v) {}
move_only_default_t (const move_only_default_t&) = delete;
move_only_default_t (move_only_default_t&& o) : val (o.val) { o.val = -1; }
move_only_default_t& operator = (const move_only_default_t&) = delete;
move_only_default_t& operator = (move_only_default_t&& o) { val = o.val; o.val = -1; return *this; }
~move_only_default_t () = default;
};
static void test_value_or_default ()
{
// Test const &
{
const result<int> r_ok = Ok(42);
hb_always_assert (r_ok.value_or_default () == 42);
const result<int> r_err = Err(ERR_A);
hb_always_assert (r_err.value_or_default () == 0);
}
// Test && (rvalues and move)
{
result<int> r_ok = Ok(42);
hb_always_assert (std::move (r_ok).value_or_default () == 42);
result<int> r_err = Err(ERR_A);
hb_always_assert (std::move (r_err).value_or_default () == 0);
}
// Test custom struct with default constructor
{
result<default_constructible_t> r_ok = default_constructible_t (456);
hb_always_assert (r_ok.value_or_default ().val == 456);
result<default_constructible_t> r_err = ERR_A;
hb_always_assert (r_err.value_or_default ().val == 123);
hb_always_assert (std::move (r_ok).value_or_default ().val == 456);
hb_always_assert (std::move (r_err).value_or_default ().val == 123);
}
// Test pointers
{
int x = 10;
result<int*> r_ok = &x;
hb_always_assert (r_ok.value_or_default () == &x);
result<int*> r_err = ERR_A;
hb_always_assert (r_err.value_or_default () == nullptr);
}
// Test move-only types
{
result<move_only_default_t> r_mo_ok = move_only_default_t (88);
hb_always_assert (std::move (r_mo_ok).value_or_default ().val == 88);
result<move_only_default_t> r_mo_err = ERR_A;
hb_always_assert (std::move (r_mo_err).value_or_default ().val == 77);
}
}
static void test_from_move()
{
{
copy_move_counter_t::reset();
copy_move_counter_t ok (24);
result<copy_move_counter_t> r_ok = result<copy_move_counter_t>::from(std::move(ok), ERR_B);
hb_always_assert (r_ok.is_ok());
hb_always_assert (r_ok->val == 24);
hb_always_assert (copy_move_counter_t::copy_count == 0);
hb_always_assert (copy_move_counter_t::move_count == 1);
}
{
copy_move_counter_t::reset();
copy_move_counter_t fail (-1);
result<copy_move_counter_t> r_fail = result<copy_move_counter_t>::from(std::move(fail), ERR_B);
hb_always_assert (r_fail.is_err());
hb_always_assert (r_fail.error() == ERR_B);
hb_always_assert (copy_move_counter_t::copy_count == 0);
hb_always_assert (copy_move_counter_t::move_count == 0);
}
}
int main ()
{
test_ok_basic ();
test_err_basic ();
test_explicit_ok_err ();
test_pointers ();
test_void ();
test_resource_cleanup ();
test_try_macro ();
test_same_or_convertible_types ();
test_error_interception ();
test_move_only_types ();
test_return_moves_from_local ();
test_from();
test_value_or_default ();
test_from_move ();
return 0;
}