Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion sdk_v2/cpp/run_coverage.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
#
# Prerequisites:
# - OpenCppCoverage installed (https://github.com/OpenCppCoverage/OpenCppCoverage)
# - A RelWithDebInfo build: .\build.bat --build --parallel
# - A Debug (or RelWithDebInfo) build: python build.py --build --config Debug --parallel
# Debug is preferred for coverage — its line tables map 1:1 to source lines, so per-line
# hit/miss reporting is more accurate than an optimized RelWithDebInfo build.
#
# Usage:
# .\run_coverage.ps1 # default Debug for better matching of lines
Expand Down Expand Up @@ -63,6 +65,10 @@ $unitCov = Join-Path $covDir "unit.cov"
$unitArgs = $commonArgs + @("--export_type", "binary:$unitCov", "--")
$unitArgs += $unitExe

# Include DISABLED_ tests (e.g. the real-network http_download manifest test) so the unit step
# covers code paths that are network-gated off by default. Mirrors the integration step below.
$unitArgs += "--gtest_also_run_disabled_tests"

if ($TestFilter) {
$unitArgs += "--gtest_filter=$TestFilter"
}
Expand Down
1 change: 1 addition & 0 deletions sdk_v2/cpp/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ add_executable(foundry_local_tests
internal_api/file_uri_test.cc
internal_api/file_writer_test.cc
internal_api/genai_config_test.cc
internal_api/http_download_test.cc
internal_api/http_retry_test.cc
internal_api/item_test.cc
internal_api/local_model_scanner_test.cc
Expand Down
114 changes: 114 additions & 0 deletions sdk_v2/cpp/test/internal_api/http_download_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
//
// Real-network tests for HttpDownloadFile — the generic HTTPS file downloader used by the
// execution-provider bootstrappers (WebGPU / CUDA). These are DISABLED by default because they
// require network access; the coverage run enables them via --gtest_also_run_disabled_tests.
//
// The positive case downloads the real WebGPU EP zip from the production CDN and validates the
// full binary download path (curl transport + Content-Length + progress + success). The negative
// case targets an unresolvable host so the failure path is deterministic and independent of any
// server's error-page behavior.
#include "http/http_download.h"

#include "logger.h"

#include <gtest/gtest.h>

#include <atomic>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

namespace fs = std::filesystem;
using namespace fl;

namespace {

// Production WebGPU EP package URL used for exercising large binary downloads.
constexpr const char* kWebGpuZipUrl =
"https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_0.1.0_win-x64.zip";

constexpr const char* kUserAgent = "FoundryLocal";

/// Captures log output so a failed download surfaces the downloader's own diagnostics.
class RecordingLogger : public ILogger {
public:
void Log(LogLevel level, std::string_view message) override {
entries.emplace_back(level, std::string(message));
}

std::string Dump() const {
std::ostringstream oss;
for (const auto& [level, msg] : entries) {
oss << " [" << static_cast<int>(level) << "] " << msg << "\n";
}
return oss.str();
}

std::vector<std::pair<LogLevel, std::string>> entries;
};

/// RAII temp file that removes itself on construction and destruction so each run starts clean.
class TempFile {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will consolidate this with changes in #851 depending on which PR goes in first.

public:
explicit TempFile(const std::string& name) {
path_ = fs::temp_directory_path() / name;
std::error_code ec;
fs::remove(path_, ec);
}

~TempFile() {
std::error_code ec;
fs::remove(path_, ec);
}

const fs::path& path() const { return path_; }

private:
fs::path path_;
};

} // namespace

// Downloads the real WebGPU EP zip and validates the success path end-to-end: returns
// true, writes a non-empty file, and reports a terminal 100% progress callback.
TEST(DISABLED_HttpDownload, DownloadsWebGpuZip) {
RecordingLogger logger;
TempFile dest("fl_webgpu_ep_test.zip");

std::vector<float> progress;
std::atomic<bool> cancel{false};

bool ok = HttpDownloadFile(
kWebGpuZipUrl, dest.path(), kUserAgent, &cancel,
[&progress](float pct) { progress.push_back(pct); },
logger);

ASSERT_TRUE(ok) << "WebGPU zip download failed. Logger output:\n"
<< logger.Dump();

ASSERT_TRUE(fs::exists(dest.path()));
EXPECT_GT(fs::file_size(dest.path()), 0u);

// The final progress callback is always 100% on success.
ASSERT_FALSE(progress.empty());
EXPECT_FLOAT_EQ(progress.back(), 100.0f);
}

// A transport failure (unresolvable host) returns false and leaves no output file behind.
// The reserved `.invalid` TLD (RFC 2606) never resolves, so the DNS failure is deterministic and
// fast regardless of network conditions to real hosts.
TEST(DISABLED_HttpDownload, ReturnsFalseAndWritesNoFileOnUnresolvableHost) {
RecordingLogger logger;
TempFile dest("fl_webgpu_zip_unresolvable.zip");

bool ok = HttpDownloadFile("https://foundry-local-test.invalid/webgpu_ep_0.1.0_win-x64.zip", dest.path(),
kUserAgent, /*cancel_flag=*/nullptr, /*progress_cb=*/{}, logger);

EXPECT_FALSE(ok);
EXPECT_FALSE(fs::exists(dest.path()));
}
18 changes: 14 additions & 4 deletions sdk_v2/cpp/test/sdk_api/download_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,15 @@ class DISABLED_DownloadFixture : public ::testing::Test {
};

TEST_F(DISABLED_DownloadFixture, RemoveAndRedownloadSmallestModel) {
// Find the smallest CPU model that is NOT already loaded by SharedTestEnv.
// Iterate all variants of each alias group — GetModels() only returns the
// selected variant, which may hide smaller CPU variants behind a GPU pick.
// Find the smallest CPU model to remove+redownload, EXCLUDING the models the shared
// environment reserves for its modality fixtures. Those (e.g. whisper for the audio
// fixture, qwen for chat) are loaded by other suites; removing one here would race the
// fixture that depends on it. Skipping the whole reserved set — not just "not currently
// loaded" — is what makes this robust: a reserved model may be unloaded right now but
// loaded by a later suite. Iterate all variants of each alias group — GetModels() only
// returns the selected variant, which may hide smaller CPU variants behind a GPU pick.
auto reserved = SharedTestEnv::Get().ReservedModels();

foundry_local::IModel* target = nullptr;
int64_t target_size = std::numeric_limits<int64_t>::max();

Expand All @@ -42,6 +48,10 @@ TEST_F(DISABLED_DownloadFixture, RemoveAndRedownloadSmallestModel) {
continue;
}

if (reserved.count(m.get()) != 0) {
continue;
}

auto variants = m->GetVariants();
for (const auto& v : variants) {
auto vi = v->GetInfo();
Expand All @@ -59,7 +69,7 @@ TEST_F(DISABLED_DownloadFixture, RemoveAndRedownloadSmallestModel) {
}
}

ASSERT_NE(target, nullptr) << "No unloaded CPU model found in catalog";
ASSERT_NE(target, nullptr) << "No unreserved, unloaded CPU model found in catalog";

auto info = target->GetInfo();
std::cout << "Download test model: " << info.Name()
Expand Down
15 changes: 15 additions & 0 deletions sdk_v2/cpp/test/sdk_api/shared_test_env.h
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,21 @@ class SharedTestEnv : public ::testing::Environment {
bool has_cuda() const { return has_cuda_; }
bool has_webgpu() const { return has_webgpu_; }

// Models this environment reserves for its modality fixtures (chat, tool, audio, streaming
// audio, embeddings, vision, reasoning). Returned regardless of current load state: a reserved
// model may be unloaded right now but loaded by a later suite via AcquireModels(). Tests that
// mutate the cache (remove/redownload) must skip this set so they don't race the fixtures that
// depend on these models.
std::set<foundry_local::IModel*> ReservedModels() const {
std::set<foundry_local::IModel*> reserved;
for (auto* p : AllSelectedModels()) {
if (p) {
reserved.insert(p);
}
}
return reserved;
}

private:
// Return `m` only if the suite acquired it. Reflects "did
// SetUpTestSuite ask for this and did the load succeed?", NOT the
Expand Down