monoslicer/Tests/DragAndDropTest.cpp

215 lines
No EOL
7.5 KiB
C++

// Regression test: dropping an audio file onto the plugin editor.
//
// Drag and drop discovery is driven entirely by RTTI. JUCE's
// ComponentPeer::DragHelpers::findDragAndDropTarget walks up the component
// chain and does dynamic_cast<FileDragAndDropTarget*> on each candidate, so the
// interface only has to be visible as a *public* base. Inheriting it privately
// makes the cast return null and the drop handlers are never called - silently,
// with no warning at build time.
//
// Build and run:
// cmake --build build --target MonoslicerDnDTest
// ./build/MonoslicerDnDTest_artefacts/Release/MonoslicerDnDTest
#include "../Source/PluginEditor.h"
#include "../Source/PluginProcessor.h"
#include <iostream>
namespace
{
int failures = 0;
void check (bool condition, const juce::String& what)
{
std::cout << (condition ? " \033[32mPASS\033[0m " : " \033[1;31mFAIL\033[0m ")
<< what << std::endl;
if (!condition)
++failures;
}
// Mirrors JUCE's DragHelpers::findDragAndDropTarget (juce_ComponentPeer.cpp).
// Returns the winning *component* rather than a FileDragAndDropTarget* so that
// this compiles - and fails at runtime rather than at build time - even when the
// interface is inherited privately. That is precisely the regression being
// guarded against: JUCE casts from Component*, which is always legal.
juce::Component* findDragTargetComponent (juce::Component* c,
const juce::StringArray& files)
{
for (; c != nullptr; c = c->getParentComponent())
{
if (auto* target = dynamic_cast<juce::FileDragAndDropTarget*> (c))
if (target->isInterestedInFileDrag (files))
return c;
}
return nullptr;
}
// Mirrors the host structure: JUCE's VST3 wrapper puts the plugin editor inside a
// ContentWrapperComponent and adds *that* to the desktop
// (juce_audio_plugin_client_VST3.cpp:1996). The peer therefore knows nothing about
// the editor directly - it can only reach it by walking the component chain.
struct HostWrapper final : juce::Component
{
explicit HostWrapper (juce::Component& child)
{
addAndMakeVisible (child);
setSize (child.getWidth(), child.getHeight());
}
void paint (juce::Graphics& g) override { g.fillAll (juce::Colours::black); }
};
void dumpChain (juce::Component* c)
{
std::cout << " chain:";
for (; c != nullptr; c = c->getParentComponent())
std::cout << " \"" << juce::String (typeid (*c).name()).upToLastOccurrenceOf ("::", true, false)
<< "\""
<< (c->isVisible() ? "" : "(hidden)")
<< (dynamic_cast<juce::FileDragAndDropTarget*> (c) != nullptr ? "[fdnd]" : "")
<< " " << c->getWidth() << "x" << c->getHeight();
std::cout << std::endl;
}
// The editor scales itself via setTransform() in resized(); at the base size
// that transform is the identity, which keeps pointer maths straightforward.
constexpr int baseWidth = 920;
constexpr int baseHeight = 606;
juce::File writeTestWav()
{
auto file = juce::File::getSpecialLocation (juce::File::tempDirectory)
.getChildFile ("monoslicer_dnd_test.wav");
constexpr double sampleRate = 44100.0;
constexpr int numSamples = 4096;
juce::AudioBuffer<float> buffer (1, numSamples);
for (int i = 0; i < numSamples; ++i)
buffer.setSample (0, i, std::sin (i / 20.0f));
juce::WavAudioFormat format;
std::unique_ptr<juce::FileOutputStream> out (file.createOutputStream());
if (out == nullptr)
{
std::cout << " \033[1;31mFAIL\033[0m could not create " << file.getFullPathName() << std::endl;
++failures;
return {};
}
{
// The writer has to be destroyed before the stream it writes to.
std::unique_ptr<juce::AudioFormatWriter> writer (
format.createWriterFor (out.get(), sampleRate, 1, 16, {}, 0));
if (writer == nullptr)
{
std::cout << " \033[1;31mFAIL\033[0m could not create a WAV writer" << std::endl;
++failures;
return {};
}
writer->writeFromAudioSampleBuffer (buffer, 0, numSamples);
}
out.release(); // commits and closes the file
if (file.getSize() == 0)
{
std::cout << " \033[1;31mFAIL\033[0m test WAV was written empty" << std::endl;
++failures;
return {};
}
return file;
}
}
int main()
{
juce::ScopedJuceInitialiser_GUI juceInitialiser;
std::cout << "\n\033[1mMonoslicer - file drag and drop\033[0m\n\n";
const auto wavFile = writeTestWav();
if (!wavFile.existsAsFile())
return 1;
const auto path = wavFile.getFullPathName();
const juce::Point<int> pointerPos (400, 300);
MonoslicerProcessor processor;
MonoslicerEditor editor (processor);
editor.setSize (baseWidth, baseHeight);
HostWrapper wrapper (editor);
wrapper.addToDesktop (0);
wrapper.setVisible (true);
auto* peer = wrapper.getPeer();
std::cout << "\n\033[1mRTTI visibility\033[0m\n";
check (dynamic_cast<juce::FileDragAndDropTarget*> (static_cast<juce::Component*> (&editor)) != nullptr,
"editor exposes FileDragAndDropTarget as a public base");
std::cout << "\n\033[1mAcceptance\033[0m\n";
check (editor.isInterestedInFileDrag ({ path }), "accepts a .wav file");
check (editor.isInterestedInFileDrag ({ "/tmp/notes.txt" }) == false, "rejects a .txt file");
std::cout << "\n\033[1mTarget discovery (as used by JUCE)\033[0m\n";
auto* underPointer = peer != nullptr ? peer->getComponent().getComponentAt (pointerPos)
: wrapper.getComponentAt (pointerPos);
if (underPointer == nullptr)
std::cout << " (no component under the pointer)" << std::endl;
dumpChain (underPointer);
check (findDragTargetComponent (underPointer, { path }) == static_cast<juce::Component*> (&editor),
"lookup from the component under the pointer finds the editor");
check (findDragTargetComponent (underPointer, { "/tmp/notes.txt" }) == nullptr,
"lookup for an unsupported file finds no target");
std::cout << "\n\033[1mEnd-to-end drop via a real native peer\033[0m\n";
if (peer != nullptr)
{
juce::ComponentPeer::DragInfo info;
info.files.add (path);
info.position = pointerPos;
check (peer->handleDragMove (info), "peer accepts the drag (handleDragMove)");
check (peer->handleDragDrop (info), "peer accepts the drop (handleDragDrop)");
// handleDragDrop() defers filesDropped() through MessageManager::callAsync
juce::MessageManager::getInstance()->runDispatchLoopUntil (250);
check (processor.getSampleBuffer().getNumSamples() == 4096,
"the dropped sample was decoded into the processor");
check (processor.getCurrentFile() == wavFile,
"the dropped file is recorded as the current file");
// Note: dropping does not auto-slice - that is a separate explicit action,
// and matches the behaviour of the "Load" button.
check (processor.getSampleRateLoaded() == 44100.0,
"the reader set the sample rate of the dropped file");
}
else
{
std::cout << " \033[33mSKIP\033[0m no native peer available (headless session)"
<< std::endl;
}
std::cout << "\n" << (failures == 0 ? "\033[32;1mAll checks passed.\033[0m"
: juce::String (failures) + " check(s) failed.")
<< "\n\n";
return failures == 0 ? 0 : 1;
}