|
9 | 9 | #include <executorch/extension/module/module.h> |
10 | 10 |
|
11 | 11 | #include <array> |
| 12 | +#include <cstring> |
12 | 13 | #include <thread> |
| 14 | +#include <variant> |
13 | 15 |
|
14 | 16 | #include <gtest/gtest.h> |
15 | 17 |
|
@@ -663,6 +665,115 @@ TEST_F(ModuleTest, TestLoadWithEmptyLoadBackendOptionsMap) { |
663 | 665 | EXPECT_TRUE(module.is_loaded()); |
664 | 666 | } |
665 | 667 |
|
| 668 | +TEST_F(ModuleTest, TestLoadWithBackendOptionsRollbackOnFailure) { |
| 669 | + // Module pointed at a non-existent file so `load_internal` will fail. |
| 670 | + Module module("/this/path/should/not/exist.pte"); |
| 671 | + |
| 672 | + { |
| 673 | + // `bo1` lives only in this scope. The Module deep-copies the input, |
| 674 | + // so dropping `bo1` is always safe regardless of whether the load |
| 675 | + // succeeded, but on the failure path we additionally verify the |
| 676 | + // Module did NOT install the input options (transactional contract). |
| 677 | + LoadBackendOptionsMap bo1; |
| 678 | + BackendOptions<2> opts; |
| 679 | + opts.set_option("rollback_test", true); |
| 680 | + ASSERT_EQ(bo1.set_options("RollbackBackend", opts.view()), Error::Ok); |
| 681 | + |
| 682 | + const auto load_error = module.load(bo1); |
| 683 | + EXPECT_NE(load_error, Error::Ok); |
| 684 | + EXPECT_FALSE(module.is_loaded()); |
| 685 | + } |
| 686 | + // `bo1` is destroyed. Module must remain in a usable state and a |
| 687 | + // subsequent `load_method` should fail with the same load-time error |
| 688 | + // (file not found) rather than crashing. |
| 689 | + EXPECT_FALSE(module.is_loaded()); |
| 690 | + const auto method_error = module.load_method("forward"); |
| 691 | + EXPECT_NE(method_error, Error::Ok); |
| 692 | + EXPECT_FALSE(module.is_method_loaded("forward")); |
| 693 | +} |
| 694 | + |
| 695 | +TEST_F(ModuleTest, TestLoadDeepCopiesBackendOptionsInputCanBeReleased) { |
| 696 | + // Pin the deep-copy contract: the caller may release the input |
| 697 | + // LoadBackendOptionsMap (and the BackendOption arrays its Spans |
| 698 | + // referenced) immediately after `load()` returns. A subsequent |
| 699 | + // `load_method` must use the Module-owned copy via the fallback path, |
| 700 | + // not dereference the released input. |
| 701 | + Module module(model_path_); |
| 702 | + |
| 703 | + { |
| 704 | + LoadBackendOptionsMap bo; |
| 705 | + BackendOptions<2> opts; |
| 706 | + opts.set_option("persist_test", true); |
| 707 | + ASSERT_EQ(bo.set_options("PersistBackend", opts.view()), Error::Ok); |
| 708 | + |
| 709 | + ASSERT_EQ(module.load(bo), Error::Ok); |
| 710 | + // `bo` and `opts` go out of scope here; their storage is freed. |
| 711 | + } |
| 712 | + |
| 713 | + // load_method without explicit backend_options falls back to the |
| 714 | + // Module's stored copy. With the old borrowed-pointer design this |
| 715 | + // would have been a use-after-free; with deep-copy it is safe. |
| 716 | + EXPECT_EQ(module.load_method("forward"), Error::Ok); |
| 717 | + EXPECT_TRUE(module.is_method_loaded("forward")); |
| 718 | + |
| 719 | + // Forward should still execute correctly using the Module-owned |
| 720 | + // backend options. |
| 721 | + auto tensor = make_tensor_ptr({2, 2}, {1.f, 2.f, 3.f, 4.f}); |
| 722 | + const auto result = module.execute("forward", {tensor, tensor, 1.0}); |
| 723 | + EXPECT_EQ(result.error(), Error::Ok); |
| 724 | +} |
| 725 | + |
| 726 | +TEST_F(ModuleTest, TestLoadStoresBackendOptionsForReadback) { |
| 727 | + // Verify that Module deep-copies the input LoadBackendOptionsMap into |
| 728 | + // its own storage so callers can both (a) release the input |
| 729 | + // immediately and (b) read back exactly what was stored via the |
| 730 | + // public `backend_options()` accessor. |
| 731 | + Module module(model_path_); |
| 732 | + |
| 733 | + // Default-constructed: no options stored yet. |
| 734 | + EXPECT_EQ(module.backend_options().size(), 0u); |
| 735 | + |
| 736 | + { |
| 737 | + LoadBackendOptionsMap bo; |
| 738 | + BackendOptions<2> opts; |
| 739 | + opts.set_option("num_threads", 8); |
| 740 | + opts.set_option("enable_profiling", true); |
| 741 | + ASSERT_EQ(bo.set_options("MyBackend", opts.view()), Error::Ok); |
| 742 | + |
| 743 | + ASSERT_EQ(module.load(bo), Error::Ok); |
| 744 | + // `bo` and `opts` go out of scope here; their backing storage is |
| 745 | + // freed. Anything we read back from `module.backend_options()` must |
| 746 | + // therefore live in Module-owned storage. |
| 747 | + } |
| 748 | + |
| 749 | + const auto& stored = module.backend_options(); |
| 750 | + ASSERT_EQ(stored.size(), 1u); |
| 751 | + |
| 752 | + const auto entry = stored.entry_at(0); |
| 753 | + EXPECT_STREQ(entry.backend_id, "MyBackend"); |
| 754 | + ASSERT_EQ(entry.options.size(), 2u); |
| 755 | + |
| 756 | + // Look up each option by key so the value assertions are direct and |
| 757 | + // independent of insertion order. |
| 758 | + const BackendOption* num_threads_opt = nullptr; |
| 759 | + const BackendOption* enable_profiling_opt = nullptr; |
| 760 | + for (const auto& opt : entry.options) { |
| 761 | + if (std::strcmp(opt.key, "num_threads") == 0) { |
| 762 | + num_threads_opt = &opt; |
| 763 | + } else if (std::strcmp(opt.key, "enable_profiling") == 0) { |
| 764 | + enable_profiling_opt = &opt; |
| 765 | + } |
| 766 | + } |
| 767 | + |
| 768 | + ASSERT_NE(num_threads_opt, nullptr); |
| 769 | + ASSERT_TRUE(std::holds_alternative<int>(num_threads_opt->value)); |
| 770 | + EXPECT_EQ(std::get<int>(num_threads_opt->value), 8); |
| 771 | + |
| 772 | + ASSERT_NE(enable_profiling_opt, nullptr); |
| 773 | + ASSERT_TRUE(std::holds_alternative<bool>(enable_profiling_opt->value)); |
| 774 | + EXPECT_TRUE(std::get<bool>(enable_profiling_opt->value)); |
| 775 | +} |
| 776 | + |
666 | 777 | TEST_F(ModuleTest, TestLoadBackendOptionsMapPersistedAcrossLoadMethod) { |
667 | 778 | Module module(model_path_); |
668 | 779 |
|
|
0 commit comments