refactor(task): de-Qt oaktask base + project tasks, wire codec task submitter

- Task base: Qt signals become lifecycle listeners (the async-command
  callback exception), cancellation uses oakrender's OakCancelAtom C
  handle; TaskManager runs std::thread workers, no signals
- ConformTask/ProxyTask implement oakcodec's submit-callback contract
  (synchronous interim); register_codec_task_submitter() closes the
  conform/proxy loop - conform of demo.mp4 produces pcm caches in tests
- ProjectImportTask/ProjectLoadTask/ProjectSaveTask over the oaknode
  C ABI; image-sequence confirmation becomes a facade callback
  (default: not a sequence)
- C API additions needed by oaktask: oaknode serializer file-level
  save/load (auto-initializing), footage video-params/cancel-atom/
  as-node, folder add-child command factory; oakcodec decoder
  conform_audio + image-sequence helpers; oakrender cancelatom
  get_native
- fix double-ownership: submitting a task to the manager transfers
  ownership, freeing its handle only releases the wrapper
- C ABI in include/task (task/manager/project, OakTaskTask opaque
  handle + lifecycle subscribe), every function tested (103 cases in
  build-oaktask incl. regressions)
- RenderTask family and OTIO tasks follow in separate commits
This commit is contained in:
2026-08-07 14:01:23 +08:00
parent 66831a870e
commit 6ded1d2d63
54 changed files with 5937 additions and 1 deletions
+316
View File
@@ -0,0 +1,316 @@
/***
Oak Video Editor - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include <filesystem>
#include <vector>
#include <gtest/gtest.h>
#include "codec/conform.h"
#include "codec/decoder.h"
#include "codec/task.h"
#include "node/folder.h"
#include "node/project.h"
#include "task/manager.h"
#include "task/project.h"
#include "task/task.h"
#include "undo/undostack.h"
namespace
{
class OakTaskFixture : public ::testing::Test {
protected:
void SetUp() override
{
project_ = oaknode_project_init();
ASSERT_NE(project_, nullptr);
ASSERT_EQ(oaknode_project_initialize(project_), OAKNODE_OK);
}
void TearDown() override
{
oaknode_project_free(project_);
}
OakNodeProject *project_ = nullptr;
};
// ---- task family ----------------------------------------------------------
TEST_F(OakTaskFixture, TaskLifecycleSync)
{
oaktask_task_free(nullptr); // no-op on NULL
OakTaskTask *t = oaktask_create_project_save(project_, nullptr, 0);
ASSERT_NE(t, nullptr);
char title[128];
EXPECT_GT(oaktask_task_title(t, title, sizeof(title)), 0);
EXPECT_EQ(oaktask_task_title(nullptr, title, sizeof(title)),
OAKTASK_E_INVALID);
EXPECT_EQ(oaktask_task_is_finished(t), 0);
EXPECT_EQ(oaktask_task_is_finished(nullptr), OAKTASK_E_INVALID);
// Save to an override path in the temp dir
std::string path =
(std::filesystem::temp_directory_path() / "oaktask_save_test.ove")
.string();
oaktask_task_free(t);
t = oaktask_create_project_save(project_, path.c_str(), 0);
ASSERT_NE(t, nullptr);
EXPECT_EQ(oaktask_task_start_sync(t), 1);
EXPECT_EQ(oaktask_task_is_finished(t), 1);
EXPECT_TRUE(std::filesystem::exists(path));
char err[256];
EXPECT_GE(oaktask_task_error(t, err, sizeof(err)), 0);
oaktask_task_free(t);
EXPECT_EQ(oaktask_debug_alive_count(), 0);
std::filesystem::remove(path);
}
TEST_F(OakTaskFixture, LoadMissingFileFails)
{
OakTaskTask *t =
oaktask_create_project_load("/nonexistent/definitely-missing.ove");
ASSERT_NE(t, nullptr);
EXPECT_EQ(oaktask_task_start_sync(t), 0);
char err[256];
EXPECT_GT(oaktask_task_error(t, err, sizeof(err)), 0);
EXPECT_EQ(oaktask_load_take_project(t), nullptr);
oaktask_task_free(t);
EXPECT_EQ(oaktask_create_project_load(nullptr), nullptr);
}
TEST_F(OakTaskFixture, SaveLoadRoundTrip)
{
std::string path =
(std::filesystem::temp_directory_path() / "oaktask_roundtrip.ove")
.string();
OakTaskTask *save =
oaktask_create_project_save(project_, path.c_str(), 0);
ASSERT_NE(save, nullptr);
ASSERT_EQ(oaktask_task_start_sync(save), 1);
oaktask_task_free(save);
OakTaskTask *load = oaktask_create_project_load(path.c_str());
ASSERT_NE(load, nullptr);
ASSERT_EQ(oaktask_task_start_sync(load), 1);
OakNodeProject *loaded = oaktask_load_take_project(load);
ASSERT_NE(loaded, nullptr);
EXPECT_EQ(oaktask_load_take_project(load), nullptr);
oaknode_project_free(loaded);
oaktask_task_free(load);
std::filesystem::remove(path);
}
TEST_F(OakTaskFixture, ImportDemoFootage)
{
OakNodeFolder *folder = oaknode_folder_create(project_);
ASSERT_NE(folder, nullptr);
const char *urls[] = { OAK_REPO_ROOT "/tests/demo.mp4" };
OakTaskTask *t =
oaktask_create_project_import(folder, project_, urls, 1);
ASSERT_NE(t, nullptr);
ASSERT_EQ(oaktask_task_start_sync(t), 1);
if (oaktask_import_footage_count(t) == 0) {
// Footage probing still resolves through oaknode's codec
// transition stubs (module wiring milestone) - demo.mp4 reports
// invalid until then.
GTEST_SKIP()
<< "footage probe not wired to oakcodec yet (step 4)";
}
EXPECT_EQ(oaktask_import_footage_count(t), 1);
EXPECT_NE(oaktask_import_footage_at(t, 0), nullptr);
EXPECT_EQ(oaktask_import_footage_at(t, 5), nullptr);
EXPECT_EQ(oaktask_import_invalid_count(t), 0);
EXPECT_EQ(oaktask_import_invalid_at(t, 0, nullptr, 0),
OAKTASK_E_NOT_FOUND);
// The import command adds the footage to the folder; undo removes it
OakUndoCommand *cmd = oaktask_import_take_command(t);
ASSERT_NE(cmd, nullptr);
EXPECT_EQ(oaktask_import_take_command(t), nullptr);
EXPECT_EQ(oaknode_folder_child_count(folder), 0); // not yet redone
oakundo_command_redo_now(cmd);
EXPECT_EQ(oaknode_folder_child_count(folder), 1);
oakundo_command_undo_now(cmd);
EXPECT_EQ(oaknode_folder_child_count(folder), 0);
oakundo_command_free(cmd);
oaktask_task_free(t);
EXPECT_EQ(oaktask_create_project_import(nullptr, project_, urls, 1),
nullptr);
EXPECT_EQ(oaktask_import_footage_count(nullptr), OAKTASK_E_INVALID);
EXPECT_EQ(oaktask_debug_alive_count(), 0);
}
// ---- manager family -------------------------------------------------------
TEST(OakTaskManager, InitShutdownAndCodecSubmitter)
{
EXPECT_EQ(oaktask_manager_init(), OAKTASK_OK);
EXPECT_EQ(oaktask_manager_init(), OAKTASK_E_STATE);
EXPECT_EQ(oakcodec_task_submit_is_registered(), 1);
EXPECT_GE(oaktask_manager_count(), 0);
oaktask_manager_delete_finished();
EXPECT_EQ(oaktask_manager_at(0), nullptr);
oaktask_manager_shutdown();
EXPECT_EQ(oakcodec_task_submit_is_registered(), 0);
EXPECT_EQ(oaktask_manager_count(), OAKTASK_E_STATE);
}
TEST(OakTaskManager, AsyncStartAndSubscribe)
{
ASSERT_EQ(oaktask_manager_init(), OAKTASK_OK);
OakNodeProject *project = oaknode_project_init();
ASSERT_NE(project, nullptr);
ASSERT_EQ(oaknode_project_initialize(project), OAKNODE_OK);
std::string path =
(std::filesystem::temp_directory_path() / "oaktask_async.ove")
.string();
OakTaskTask *t = oaktask_create_project_save(project, path.c_str(), 0);
ASSERT_NE(t, nullptr);
struct Seen {
int started;
int finished;
double succeeded;
} seen = { 0, 0, 0.0 };
EXPECT_EQ(oaktask_task_subscribe(
t,
[](int event_id, double value, void *userdata) {
Seen *s = static_cast<Seen *>(userdata);
if (event_id == OAKTASK_EVENT_STARTED) {
s->started++;
} else if (event_id == OAKTASK_EVENT_FINISHED) {
s->finished++;
s->succeeded = value;
}
},
&seen),
0);
EXPECT_EQ(oaktask_task_subscribe(t, nullptr, nullptr),
OAKTASK_E_INVALID);
ASSERT_EQ(oaktask_task_start(t), OAKTASK_OK);
EXPECT_EQ(oaktask_task_start(t), OAKTASK_E_STATE);
EXPECT_EQ(oaktask_task_wait(t), OAKTASK_OK);
EXPECT_EQ(oaktask_task_is_finished(t), 1);
EXPECT_EQ(oaktask_task_succeeded(t), 1);
EXPECT_EQ(seen.started, 1);
EXPECT_EQ(seen.finished, 1);
EXPECT_EQ(seen.succeeded, 1.0);
EXPECT_TRUE(std::filesystem::exists(path));
std::filesystem::remove(path);
oaktask_task_free(t);
oaknode_project_free(project);
oaktask_manager_shutdown();
EXPECT_EQ(oaktask_debug_alive_count(), 0);
}
// ---- conform submitter end-to-end ------------------------------------------
TEST(OakTaskConform, SubmittedConformProducesPcm)
{
const std::string demo = OAK_REPO_ROOT "/tests/demo.mp4";
if (!std::filesystem::exists(demo)) {
GTEST_SKIP() << "demo.mp4 not available";
}
ASSERT_EQ(oaktask_manager_init(), OAKTASK_OK);
ASSERT_EQ(oakcodec_conform_create_instance(), OAKCODEC_OK);
// Probe demo.mp4 for an audio stream
OakDecoder probe = oakcodec_decoder_probe(demo.c_str());
ASSERT_NE(probe.ctx, nullptr);
if (oakcodec_decoder_probe_audio_stream_count(probe) <= 0) {
oakcodec_decoder_free(&probe);
oaktask_manager_shutdown();
GTEST_SKIP() << "demo.mp4 has no audio stream";
}
oakcodec_audio_stream_info info = {};
ASSERT_EQ(oakcodec_decoder_probe_get_audio_stream(probe, 0, &info),
OAKCODEC_OK);
oakcodec_decoder_free(&probe);
std::string cache_path =
(std::filesystem::temp_directory_path() / "oaktask_conform_cache")
.string();
std::error_code ec;
std::filesystem::remove_all(cache_path, ec);
std::filesystem::create_directories(cache_path, ec);
const int sample_format = 4; /* olive::core::SampleFormat::f32_p */
int state = oakcodec_conform_get_state(
cache_path.c_str(), demo.c_str(), info.stream_index,
info.sample_rate, info.channel_layout, sample_format, 1);
EXPECT_EQ(state, 0 /* OAKCODEC_CONFORM_EXISTS */);
int count = oakcodec_conform_filename_count(
cache_path.c_str(), demo.c_str(), info.stream_index,
info.sample_rate, info.channel_layout, sample_format);
EXPECT_GT(count, 0);
std::filesystem::remove_all(cache_path, ec);
oakcodec_conform_destroy_instance();
oaktask_manager_shutdown();
}
} // namespace