mirror of
https://github.com/Netflix/dial-reference.git
synced 2026-06-08 02:49:58 +00:00
588 lines
19 KiB
C++
588 lines
19 KiB
C++
/*
|
|
* Copyright (c) 2014 Netflix, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are met:
|
|
*
|
|
* Redistributions of source code must retain the above copyright notice, this
|
|
* list of conditions and the following disclaimer.
|
|
* Redistributions in binary form must reproduce the above copyright notice,
|
|
* this list of conditions and the following disclaimer in the documentation
|
|
* and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY NETFLIX, INC. AND CONTRIBUTORS "AS IS" AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL NETFLIX OR CONTRIBUTORS BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
|
|
#include "DialConformance.h"
|
|
#include <assert.h>
|
|
#include <time.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
|
|
// Number of milliseconds to sleep between launches when doing a launch=ALL
|
|
#define LAUNCH_SLEEP 6000
|
|
#define MAX_PARAMETER_LENGTH 60
|
|
|
|
using namespace std;
|
|
|
|
/*
|
|
* This class will be reponsible for reporting the conformance test results.
|
|
* Requirements:
|
|
* * Results should be in HTML
|
|
* * Results should be easily readable
|
|
* * Results should clearly distinguish between pass and fail
|
|
* * If there is a failure, there should be ample information provided
|
|
* for debugging
|
|
*/
|
|
class OutputWriter
|
|
{
|
|
public:
|
|
OutputWriter( string fileName ) :
|
|
_filename(fileName),
|
|
_pCurrentTest(NULL),
|
|
_isComplete(false)
|
|
{
|
|
}
|
|
~OutputWriter()
|
|
{ }
|
|
|
|
void setOutputFile( string fileName )
|
|
{
|
|
_filename = fileName;
|
|
}
|
|
|
|
void setTest( string testName )
|
|
{
|
|
assert( _pCurrentTest == NULL );
|
|
_pCurrentTest = new Test(testName);
|
|
_tests.push_back(_pCurrentTest);
|
|
}
|
|
void clearTest()
|
|
{
|
|
// still holding a ref in the test vector. That will get deleted
|
|
// when the test is complete
|
|
_pCurrentTest = NULL;
|
|
}
|
|
|
|
void setError( string error ) { _pCurrentTest->addError(error); }
|
|
void setResult( bool result ) { _pCurrentTest->setResult( result ); }
|
|
|
|
void start( string& friendlyname, string& uuid, string ipaddress )
|
|
{
|
|
_file.open(_filename.c_str());
|
|
_file << "<html>\n<body>\n";
|
|
_file << "<font size=\"6\"><b>FriendlyName: " << friendlyname;
|
|
_file << "<br>\nIP: " << ipaddress;
|
|
_file << "<br>\nUUID: " << uuid;
|
|
|
|
{
|
|
time_t rawtime;
|
|
struct tm * timeinfo;
|
|
char buffer [80];
|
|
|
|
time ( &rawtime );
|
|
timeinfo = localtime ( &rawtime );
|
|
|
|
//strftime (buffer,80,"%I:%M%p.",timeinfo);
|
|
strftime (buffer,80,"%c",timeinfo);
|
|
_file << "<br>\nTime of Run: " << buffer << "</b></font>";
|
|
}
|
|
_file << "<table border=\"10\">\n";
|
|
_file << "<tr>\n";
|
|
_file << "<th> RESULT </th>"; //header 1
|
|
_file << "<th> Test Executed </th>"; //header 2
|
|
_file << "<th> Errors </th>"; //header 2
|
|
_file << "</tr>\n";
|
|
}
|
|
|
|
void complete()
|
|
{
|
|
vector<Test*>::iterator it;
|
|
for( it = _tests.begin(); it < _tests.end(); it++ )
|
|
{
|
|
write("<tr>");
|
|
// write the name and the result
|
|
stringstream result;
|
|
bool testResult = (*it)->getResult();
|
|
result << "<td><b> <font color=\"" << (testResult ? "green" : "red") << "\"> ";
|
|
result << (testResult ? "SUCCESS" : "FAIL") << " </b></font></td>\n<td>" << (*it)->getName() << "</td>";
|
|
write( result.str() );
|
|
|
|
// write errors if they exist
|
|
write("<td>");
|
|
vector<string> errors = (*it)->getErrors();
|
|
if( !errors.empty() )
|
|
{
|
|
vector<string>::iterator it2;
|
|
write("<b>");
|
|
for( it2 = errors.begin(); it2 < errors.end(); it2++ )
|
|
write( (*it2) );
|
|
write("</b>");
|
|
}
|
|
else
|
|
write("NO ERROR");
|
|
write("</td>\n</tr>");
|
|
|
|
// delete the test
|
|
delete (*it);
|
|
}
|
|
_file << "</table></body>\n</html>\n";
|
|
_file.close();
|
|
|
|
_isComplete = true;
|
|
}
|
|
|
|
private:
|
|
// Abstraction of a test
|
|
class Test
|
|
{
|
|
public:
|
|
Test(string& name) :
|
|
_testname(name),
|
|
_testPassed(false) {}
|
|
~Test() {}
|
|
|
|
string getName() { return _testname; }
|
|
void setResult( bool result ) { _testPassed = result; }
|
|
bool getResult() { return _testPassed; }
|
|
|
|
void addError( string error ) { _errors.push_back(error); }
|
|
vector<string> getErrors() { return _errors; }
|
|
|
|
private:
|
|
string _testname;
|
|
bool _testPassed;
|
|
vector<string> _errors;
|
|
};
|
|
|
|
string _filename;
|
|
ofstream _file;
|
|
vector<Test*> _tests;
|
|
Test* _pCurrentTest;
|
|
bool _isComplete;
|
|
|
|
int write( string line )
|
|
{
|
|
_file << line.c_str() << "\n";
|
|
return line.length()+1;
|
|
}
|
|
};
|
|
|
|
static const string defaultOutputFile = "./report.html";
|
|
static OutputWriter gWriter(defaultOutputFile);
|
|
|
|
DialConformance *DialConformance::sConformance = 0;
|
|
|
|
DialConformance* DialConformance::create(void)
|
|
{
|
|
assert( DialConformance::sConformance == 0 );
|
|
return new DialConformance();
|
|
}
|
|
|
|
DialConformance* DialConformance::instance(void)
|
|
{
|
|
return DialConformance::sConformance;
|
|
}
|
|
|
|
DialConformance::DialConformance()
|
|
{
|
|
assert( DialConformance::sConformance == 0 );
|
|
DialConformance::sConformance = this;
|
|
}
|
|
|
|
DialConformance::~DialConformance()
|
|
{
|
|
assert( DialConformance::sConformance == this );
|
|
DialConformance::sConformance = 0;
|
|
}
|
|
|
|
void DialConformance::extractParamValue(
|
|
string& param, string& value )
|
|
{
|
|
// TODO: Add support for quotes after the equals
|
|
size_t posBegin = param.find("=");
|
|
value = param.substr(posBegin+1, param.length()-posBegin);
|
|
}
|
|
|
|
bool DialConformance::validateParams( vector<string>& params,
|
|
string& responseHeaders, string& responseBody )
|
|
{
|
|
bool testPassed = true;
|
|
vector<string>::iterator it;
|
|
for( it = params.begin(); it < params.end() && testPassed == true; it++ )
|
|
{
|
|
ATRACE("%s:: params:%s \n", __FUNCTION__, (*it).c_str());
|
|
if( (*it).find("httpresponse=") != (*it).npos )
|
|
testPassed = validateHttpResponse( responseHeaders, *it );
|
|
if( (*it).find("httpresponseheader=") != (*it).npos )
|
|
testPassed = validateHttpHeaders( responseHeaders, *it );
|
|
if( (*it).find("resultbody=") != (*it).npos )
|
|
testPassed = validateResponseBody( responseBody, *it );
|
|
}
|
|
return testPassed;
|
|
}
|
|
|
|
|
|
bool DialConformance::validateHttpResponse(
|
|
string& headers, string& param )
|
|
{
|
|
bool retval = false;
|
|
string responseCode;
|
|
extractParamValue( param, responseCode );
|
|
|
|
ATRACE("%s: Searching for %s\n", __FUNCTION__, responseCode.c_str());
|
|
if( headers.find(responseCode) != headers.npos )
|
|
{
|
|
ATRACE("%s: %s FOUND\n", __FUNCTION__, responseCode.c_str());
|
|
retval = true;
|
|
}
|
|
else
|
|
{
|
|
ATRACE("%s: %s not found\n", __FUNCTION__, responseCode.c_str());
|
|
stringstream error;
|
|
error << responseCode << " not found\n";
|
|
gWriter.setError( error.str() );
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
bool DialConformance::validateHttpHeaders(
|
|
string& headers, string& param )
|
|
{
|
|
bool retval = false;
|
|
string responseCode;
|
|
extractParamValue( param, responseCode );
|
|
|
|
ATRACE("%s: Searching for %s\n", __FUNCTION__, responseCode.c_str());
|
|
if( headers.find(responseCode) != headers.npos )
|
|
{
|
|
ATRACE("%s: %s FOUND\n", __FUNCTION__, responseCode.c_str());
|
|
retval = true;
|
|
}
|
|
else
|
|
{
|
|
ATRACE("%s: %s not found\n", __FUNCTION__, responseCode.c_str());
|
|
stringstream error;
|
|
error << responseCode << " not found\n";
|
|
gWriter.setError( error.str() );
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
bool DialConformance::validateResponseBody(
|
|
string& body, string& param )
|
|
{
|
|
bool retval = false;
|
|
string responseCode;
|
|
extractParamValue( param, responseCode );
|
|
|
|
ATRACE("%s: Searching for %s\n", __FUNCTION__, responseCode.c_str());
|
|
if( body.find(responseCode) != body.npos )
|
|
{
|
|
ATRACE("%s: %s FOUND\n", __FUNCTION__, responseCode.c_str());
|
|
retval = true;
|
|
}
|
|
else
|
|
{
|
|
ATRACE("%s: %s not found\n", __FUNCTION__, responseCode.c_str());
|
|
stringstream error;
|
|
error << responseCode << " not found\n";
|
|
error << "response: " << body;
|
|
gWriter.setError( error.str() );
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
void DialConformance::getPayload(vector<string>& params, string& payload )
|
|
{
|
|
vector<string>::iterator it;
|
|
for( it = params.begin(); it < params.end(); it++ )
|
|
{
|
|
if( (*it).find("param") != (*it).npos )
|
|
{
|
|
// Check to see if the param is using quotes. If it is, split on
|
|
// that. If not, split on a space
|
|
size_t posEnd, pos = (*it).find( "=" );
|
|
if( (*it)[pos+1] == '\"' )
|
|
{
|
|
posEnd = (*it).find( "\"", pos+2 );
|
|
payload = (*it).substr( pos+2, posEnd-(pos+2) );
|
|
}
|
|
else
|
|
{
|
|
posEnd = (*it).find( " ", pos );
|
|
payload = (*it).substr( pos+1, posEnd );
|
|
}
|
|
ATRACE("payload = %s\n", payload.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
bool DialConformance::execute_launch( Application* pApp, vector<string>& params )
|
|
{
|
|
string payload, responseHeaders, responseBody;
|
|
getPayload( params, payload );
|
|
pApp->launch( payload, responseHeaders, responseBody );
|
|
return (validateParams( params, responseHeaders, responseBody ));
|
|
}
|
|
|
|
bool DialConformance::execute_status( Application* pApp, vector<string>& params )
|
|
{
|
|
ATRACE("%s::%d\n", __FUNCTION__, __LINE__);
|
|
string responseHeaders, responseBody;
|
|
pApp->status( responseHeaders, responseBody );
|
|
|
|
bool retval = validateParams( params, responseHeaders, responseBody );
|
|
if( retval && (responseHeaders.find("200") != responseHeaders.npos) )
|
|
{
|
|
// if the status was successful
|
|
// Ensure the response body contains the application name
|
|
if( responseBody.find( pApp->getName() ) == responseBody.npos )
|
|
{
|
|
retval = false;
|
|
stringstream error;
|
|
error << "Reponse XML did not contain application name: " << pApp->getName();
|
|
gWriter.setError( error.str() );
|
|
retval = false;
|
|
}
|
|
if( responseBody.find( "urn:dial-multiscreen-org:schemas:dial" )
|
|
== responseBody.npos )
|
|
{
|
|
gWriter.setError( "Response Body did not contain DIAL service "
|
|
"string: urn:dial-multiscreen-org:schemas:dial" );
|
|
retval = false;
|
|
}
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
bool DialConformance::execute_stop( Application* pApp, vector<string>& params )
|
|
{
|
|
ATRACE("%s::%d\n", __FUNCTION__, __LINE__);
|
|
string responseHeaders, responseBody;
|
|
pApp->stop(responseHeaders);
|
|
return (validateParams( params, responseHeaders, responseBody ));
|
|
}
|
|
|
|
DialConformance::Application* DialConformance::getApplication( string& command )
|
|
{
|
|
Application* pApp = NULL;
|
|
|
|
// extract the application
|
|
string app;
|
|
size_t pos = command.find("=");
|
|
app = command.substr(pos+1, command.length());
|
|
assert( app.length() > 0 );
|
|
|
|
// find the application
|
|
vector<Application*>::iterator it;
|
|
for( it = _apps.begin(); it < _apps.end(); it++ )
|
|
{
|
|
ATRACE("Comparing: %s.compare(%s)\n", (*it)->getName().c_str(), app.c_str());
|
|
if( (*it)->getName().compare(app) == 0)
|
|
{
|
|
pApp = (*it);
|
|
break;
|
|
}
|
|
}
|
|
|
|
assert(pApp != NULL);
|
|
return pApp;
|
|
}
|
|
|
|
bool DialConformance::execute_command( string& command, vector<string>& params )
|
|
{
|
|
ATRACE("%s::%s\n", __FUNCTION__, command.c_str());
|
|
bool retval = false;
|
|
if( command.find("launch") != command.npos )
|
|
retval = execute_launch(getApplication(command), params);
|
|
else if ( command.find("status") != command.npos )
|
|
retval = execute_status(getApplication(command), params);
|
|
else if ( command.find("stop") != command.npos )
|
|
retval = execute_stop(getApplication(command), params);
|
|
else if ( command.find("sleep") != command.npos )
|
|
{
|
|
string time;
|
|
size_t pos = command.find("=");
|
|
time = command.substr(pos+1, command.length());
|
|
unsigned long milliseconds = atoi(time.c_str());
|
|
ATRACE("Sleeping %ld milliseconds \n", milliseconds);
|
|
usleep(milliseconds*1000);
|
|
retval = true;
|
|
}
|
|
else ATRACE("Can't execute command: %s\n", command.c_str());
|
|
return retval;
|
|
}
|
|
|
|
void DialConformance::run_internal( DialServer* pServer )
|
|
{
|
|
string command;
|
|
vector<string> params;
|
|
bool retval;
|
|
string friendlyName, uuid;
|
|
|
|
// only execute the test if we have a friendly name and UUID
|
|
retval = pServer->getFriendlyName( friendlyName );
|
|
if (retval) pServer->getUuid( uuid );
|
|
if (retval) gWriter.start( friendlyName, uuid, pServer->getIpAddress() );
|
|
while( retval && _input.getNextAction(command, params) )
|
|
{
|
|
vector<string> commandlist;
|
|
|
|
// Check the command here, if it is command=ALL, then we need to loop through
|
|
// all of the valid applications
|
|
size_t pos;
|
|
if( (pos = command.find("=ALL")) != command.npos )
|
|
{
|
|
vector<Application*>::iterator it;
|
|
for( it = _apps.begin(); it < _apps.end(); it++ )
|
|
{
|
|
// strip off ALL and append the application name
|
|
string newCommand = command.substr(0, pos+1);
|
|
newCommand.append( (*it)->getName() );
|
|
Application *pApp = getApplication( newCommand );
|
|
if( pApp != NULL && (!pApp->isErrorApp()) )
|
|
{
|
|
ATRACE("Adding command %s\n", newCommand.c_str());
|
|
commandlist.push_back( newCommand );
|
|
}
|
|
|
|
if( newCommand.find("launch") != newCommand.npos )
|
|
{
|
|
// If we are adding launch commands, insert a sleep so that we don't overwhelm
|
|
// the target
|
|
stringstream sleep;
|
|
sleep << "sleep=" << LAUNCH_SLEEP;
|
|
commandlist.push_back( sleep.str() );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ATRACE("Adding command %s\n", command.c_str());
|
|
commandlist.push_back(command);
|
|
}
|
|
|
|
vector<string>::iterator it;
|
|
for( it = commandlist.begin(); it < commandlist.end(); it++ )
|
|
{
|
|
// construct the name of the test
|
|
ATRACE("***********************" "\n%s: Command: %s \n",
|
|
__FUNCTION__, (*it).c_str());
|
|
stringstream test;
|
|
test << "Command:" << (*it);
|
|
|
|
// The input code will always push a parameter. If there are no
|
|
// parameters, it will push an empty string, so test for that here.
|
|
if( params.size() > 0 && !params[0].empty() ) test << " Params:";
|
|
|
|
vector<string>::iterator it2;
|
|
for( it2 = params.begin(); it2 < params.end(); it2++)
|
|
{
|
|
ATRACE("param: %s ", (*it2).c_str());
|
|
|
|
// check to see if we have a really large parameter, if we do,
|
|
// truncate it to a reasonable length
|
|
string param = (*it2);
|
|
if( param.size() > MAX_PARAMETER_LENGTH )
|
|
{
|
|
param = (*it2).substr(0, MAX_PARAMETER_LENGTH-3 );
|
|
param.append("...");
|
|
}
|
|
test << param << " ";
|
|
}
|
|
|
|
if( (*it).find("sleep") == (*it).npos )
|
|
{
|
|
// set the test name
|
|
gWriter.setTest( test.str() );
|
|
|
|
// run the test
|
|
bool retval = execute_command( *it, params );
|
|
printf("%s\n", retval ? "SUCCESS":"FAILED");
|
|
|
|
// set the result and prep for the next test
|
|
gWriter.setResult( retval );
|
|
gWriter.clearTest();
|
|
}
|
|
else
|
|
{
|
|
//just sleep here
|
|
string time;
|
|
size_t pos = (*it).find("=");
|
|
time = (*it).substr(pos+1, command.length());
|
|
unsigned long milliseconds = atoi(time.c_str());
|
|
ATRACE("Sleeping %ld milliseconds \n", milliseconds);
|
|
usleep(milliseconds*1000);
|
|
}
|
|
}
|
|
|
|
// clear the parameters for the next loop
|
|
params.clear();
|
|
}
|
|
if( retval ) gWriter.complete();
|
|
}
|
|
|
|
int DialConformance::run(
|
|
DialServer* pServer,
|
|
vector<string>& appList,
|
|
string &inputFile,
|
|
string &outputFile )
|
|
{
|
|
printf("Running conformance tests from %s and printing report to %s\n\n",
|
|
inputFile.empty() ?
|
|
DialClientInput::getDefaultFilename().c_str() :
|
|
inputFile.c_str(),
|
|
outputFile.empty() ?
|
|
defaultOutputFile.c_str() :
|
|
outputFile.c_str() );
|
|
//read the input file
|
|
_input.init(inputFile);
|
|
if( !outputFile.empty() ) gWriter.setOutputFile( outputFile );
|
|
|
|
// Add applications passed by the client (command line)
|
|
vector<string>::iterator it;
|
|
for( it = appList.begin(); it < appList.end(); it++ ) _input.addApplication(*it);
|
|
|
|
// build the application list
|
|
vector<string> listOfApps;
|
|
_input.getApplicationList(listOfApps); // get the list of apps
|
|
for( it = listOfApps.begin(); it < listOfApps.end(); it++ )
|
|
{
|
|
_apps.push_back( new Application(*it, pServer, false) );
|
|
ATRACE("Adding application: %s\n", (*it).c_str());
|
|
}
|
|
|
|
_input.getErrorApplicationList(listOfApps); // get the list of apps
|
|
for( it = listOfApps.begin(); it < listOfApps.end(); it++ )
|
|
{
|
|
_apps.push_back( new Application(*it, pServer, true) );
|
|
ATRACE("Adding Error application: %s\n", (*it).c_str());
|
|
}
|
|
|
|
// run the test
|
|
run_internal( pServer );
|
|
|
|
// clean up the application list
|
|
vector<Application*>::iterator appit;
|
|
for ( appit = _apps.begin() ; appit < _apps.end(); appit++ ) delete (*appit);
|
|
|
|
return 0;
|
|
}
|
|
|