blob: 1aa80809283d7ad3d231bb9aa05cb25479728dae [file] [log] [blame]
/*
* 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;
}