Related
My imported metadata has a pre-defined nested structure (an example show in the following), which is a single string after imported to DM.
The whole metadata and each branch level are included in brace marks {}, all the key and key values are included by quotation marks "" and deliminated by colon :
My question is, how to convert the data and wrap them into a TagGroup object so that indexing, searching and data accessing operations can be done much easier?
Thanks!
Here is an example:
{
"Acquisition": {
"AcquisitionStartDatetime": {
"DateTime": "1473763749"
},
"AcquisitionDatetime": {
"DateTime": "0"
},
"BeamType": "",
"SourceType": "Monochromator"
},
"BinaryResult": {
"AcquisitionUnit": "",
"CompositionType": "",
"DetectorIndex": "3",
"Detector": "HAADF",
"PixelSize": {
"width": "5.408370946750477e-010",
"height": "5.408370946750477e-010"
},
"PixelUnitX": "m",
"PixelUnitY": "m",
"Offset": {
"x": "-2.769085924736244e-007",
"y": "-2.769085924736244e-007"
},
"Encoding": ""
},
"Sample": "",
"GasInjectionSystems": ""
}
As Mike has pointed out in the comments, this is rather a tedious than difficult task. Best to create a little parser script in a separate class which turns formats
"NAME: { into TagGroups of label NAME plus an increased hierachy level.
and
"NAME": "VALUE" into Tags of label NAME and value VALUE.
and
} into a 'reduce hierachy level' step.
Note, that you can always use String when creating the taggroup, even if you want to read it out as number at a later time point.
Recursivly browse and remember the "taggroup-level" you are in, so that each new tag is added at that level. Skip invalid text sections.
The F1 help documenation of DigitalMicrograph has a section on strings, which lists the commands you're most likely to need:
String StringAppend( String s1, String s2 )
Number StringCompare( String s1, String s2 )
Boolean StringIsValid( String str )
String StringToLower( String str )
String StringToUpper( String str )
Number len( String str )
String left( String str, Number count )
String mid( String str, Number offset, Number count )
String right( String str, Number count )
Number find( String s1, String s2 )
Number val( String str )
Additionally, I find it sometimes useful to user the tert-operator for strings like in
number isOK = 1
string str = isOK == 1 ? "true" : "false"
Also, when parsing, watch out for tabulator and line return characters. (Use \t and \n to search for them. You might need to use "\n" and "\t" when specifying int in a string, as \ will be interpreted as control character.)
Edit: Fixed code now
I'm sure some a more cleaned up version is possible, but it does the task:
Class CMetaStringToTagGroup
{
// Find next string bracketed by " in input string, starting search
// at given index. Returns string and end-position of search
string FindNextKeyName( object self, string input, number & searchPos )
{
number totalLength = len( input )
number start = 0, end = 0
while( searchPos < totalLength )
{
searchpos++
if ( "\"" == input.mid(searchpos-1,1) )
{
if ( !start )
start = searchpos-1
else
{
end = searchpos-1
return input.mid(start+1,end-start-1)
}
}
}
return ""
}
// Returns the next of either "{" , "}" or """ after a collon ":"
string GetNextIndicator( object self, string input, number & searchPos )
{
number totalLength = len( input )
while( searchPos < totalLength )
{
searchpos++
if ( "{" == input.mid(searchpos-1,1) )
return "{"
if ( "}" == input.mid(searchpos-1,1) )
return "}"
if ( "\"" == input.mid(searchpos-1,1) )
return "\""
}
return ""
}
// In a tag-path string, find location of last colon
number findLastColon( object self, string input )
{
number totalLength = len( input )
number lastPos = -1
number searchPos = 0
while( searchPos < totalLength )
{
searchpos++
if ( ":" == input.mid(searchpos-1,1) )
lastPos = searchpos-1
}
return lastPos
}
// Parse textstring and create taggroup from it
TagGroup CreateTagFromText( object self, string input )
{
TagGroup rootTG = NewTagGroup()
string currentPath = ""
number totalLength = len( input )
number searchPos = 0
number searchPos2
string keyName, indicator
while( searchPos < totalLength )
{
// search for new key or closing bracket, whatever first
searchPos2 = searchPos
indicator = self.GetNextIndicator( input, searchPos2 )
keyName = self.FindNextKeyName( input, searchPos )
if ( ( "}" == indicator ) && (searchpos2<searchPos ) )
{
// decrease hierachy
number cutPos = self.findLastColon( currentPath )
currentPath = left( currentPath, cutPos )
result("\n DEC ")
searchPos = searchPos2
}
else
{
// Either add value or new sub-tagGroup
if ( "" == keyname ) break; // No more keys found
indicator = self.GetNextIndicator( input, searchPos )
if ( "" == indicator ) break; // No more indicator found -- should not happen!
if ( "{" == indicator )
{
// increase hierachy
currentPath += ":" + keyname
rootTg.TagGroupSetTagAsTagGroup( currentPath, NewTagGroup() )
result("\n INC ("+keyname+")")
}
else if ( "\"" == indicator )
{
// Add value
searchPos--
string valStr = self.FindNextKeyName( input, searchPos )
rootTg.TagGroupSetTagAsString( currentPath + ":" + keyname, valStr )
result("\n VAL("+keyname+") ")
}
}
}
return rootTg
}
}
{
// Reading input text
number fileID = OpenFileForReading("C:\\test.txt")
object fStream = NewStreamFromFileReference(fileID,1)
string inputStr = fStream.StreamReadAsText(0, fStream.StreamGetSize())
// Parsing text
number searchPos = 0
TagGroup con = alloc(CMetaStringToTagGroup).CreateTagFromText( inputStr )
con.TagGroupopenBrowserwindow("",0)
}
I am writing in DigialMicrograph scripting. I want a script to open more than one image in a single dialog, a multi-select image dialog, similar to when you go to any Windows open dialog, select several images and press Ok.
I think that this is possible but I haven't found a way to do it. I want to introduce this specific code in my script, to avoid image-opening prior to running the script.
I know the function OpenDialog but this only allows opening of a single image. Could anybody show me some script or a function which can help me further?
Thanks.
What you are asking for might be a bit more involved. There is no multiple open function in the scripting language, so you can only create it yourself, involving multiple steps:
An input for a base-folder (This you could prompt the user for with a GetDirectoryDialog dialog)
Using the command GetFilesInDirectory to retrieve a list (TagList) of files in that folder
Process that list to filter out what you are interested in
Build a custom-dialog (derived from UIFrame) which lists the files with either checkboxes, multiple drop-down menus, or a list-box to select from.
Act on the selection made in your custom dialog, i.e. open all those files
The following script is an example doing that.
class MyMultiSelect : UIframe
{
string root
TagGroup FileList
TagGroup DLGlist
TagGroup CreateDLG( object self )
{
TagGroup dlg,dlgitems
dlg = DLGCreateDialog( "MultiOpen", dlgItems )
number nMax = fileList.TagGroupCountTags();
TagGroup listitems
DLGlist = DLGCreateList( listitems, 90, nMax+1 )
DLGlist.DLGMultipleSelect(1)
for (number i=0; i<nMax; i++)
{
string name
if ( FileList.TagGroupGetIndexedTagAsString(i,name) )
listitems.DLGAddListItem( name, 0 )
}
dlgitems.DLGAddElement(DLGlist)
return dlg
}
TagGroup CreateFilteredFileList( object self )
{
// Get all files
TagGroup list = GetFilesInDirectory( root, 1 )
// Filter all DM3 files
TagGroup filtered = NewTagList()
for( number i = 0; i<list.TagGroupCountTags(); i++)
{
TagGroup entry
string file = ""
list.TagGroupGetIndexedTagAsTagGroup(i,entry)
entry.TagGroupGetTagAsString( "Name", file )
if ( -1 != find(file,".dm3") )
filtered.TagGroupInsertTagAsString(Infinity(),file)
}
return filtered
}
TagGroup GetSelectedList( object self )
{
TagGroup selList = NewTagList()
TagGroup itemList
DLGlist.TagGroupGetTagAsTagGroup( "Items", itemList )
for ( number i=0; i<itemList.TagGroupCountTags(); i++ )
{
number isSelected = 0
TagGroup entry
itemList.TagGroupGetIndexedTagAsTagGroup(i,entry)
entry.TagGroupGetTagAsBoolean( "Selected", isSelected )
if ( isSelected )
{
string filename
entry.TagGroupGetTagAsString( "Label", fileName )
selList.TagGroupInsertTagAsString( Infinity(),fileName)
}
}
return selList
}
void OpenSelectedFiles( object self )
{
TagGroup files = self.GetSelectedList()
number nFiles = files.TagGroupCountTags()
if ( 0 == nFiles )
Throw( "No files selected" )
Result( "\n Opening "+nFiles+" files now..")
for ( number i=0; i<nFiles; i++ )
{
string filename
files.TagGroupGetIndexedTagAsString(i,fileName)
string path = root + "\\" + filename
Result( "\n Opening: "+path)
OpenImage(path).ShowImage()
}
}
Object Init( object self, string rootInput )
{
root = rootInput
FileList = self.CreateFilteredFileList( )
return self.super.Init( self.CreateDLG() )
}
}
// Main
{
string rootDir
GetDirectoryDialog( NULL, "Select Folder from which to multi-open", GetApplicationDirectory("current",0), rootDir )
object dlg = Alloc(MyMultiSelect).Init(rootDir)
dlg.pose()
dlg.OpenSelectedFiles()
}
I have a set of functions inside a class that I need to define. Each passes a different value into another function:
void function00(object self, taggroup tg) self.otherfunction(tg,0,0)
void function01(object self, taggroup tg) self.otherfunction(tg,0,1)
void function02(object self, taggroup tg) self.otherfunction(tg,0,2)
void function03(object self, taggroup tg) self.otherfunction(tg,0,3)
void function04(object self, taggroup tg) self.otherfunction(tg,0,4)
I have 100 of these functions and I'd prefer not to define each one separately. Considering the above example I'd like to do something like:
for(number i=0; i<5; i++){
void function0+i(object self, taggroup tg) self.otherfunction(tg,0,i)
}
which doesn't work on it's own. Any suggestions?
For some more context I create a series of check boxes inside 2 for loops with the following:
BOXinsides.DLGAddElement(DLGCreateCheckBox(label,0,"function"+i+j).DLGIdentifier("#function"+i+j))
and I need to define all the functions in some sensible way.
DigitalMicrograph scripting does not allow this type of template code. However, you can solve your problem by linking all checkbox items to the same action-method. The signature of the action method passed in the TagGroup which is the checkbox item itself. You can use this to derive information from it, for example by looking at a checkbox property such as its title:
class myUI : UIframe
{
void generalFunction( object self , tagGroup checkTg )
{
// checkTg is the taggroup of the checkbox which fired the method.
// Use its Title to get back the running value!
string label = checkTg.DLGGetTitle()
Result( "\n label of checkbox:" + label )
number i = val( right( label, len( label ) - 1 ) )
Result( "\n running index:" + i )
}
TagGroup CreateCheckboxes( object self )
{
TagGroup checkboxGroup = DLGCreateGroup()
for ( number i = 0 ; I < 5 ; i++ )
{
checkboxGroup.DLGAddElement( DLGCreateCheckBox( "C" + I , 0 , "generalFunction" ) )
}
return checkboxGroup
}
TagGroup CreateDLGTags( object self )
{
TagGroup dlg, dlgitems
dlg = DLGCreateDialog( "Test" , dlgitems )
dlgitems.DLGAddElement( self.CreateCheckboxes() )
return dlg
}
object Init( object self )
{
return self.super.init( self.CreateDLGTags() )
}
}
// MAIN SCRIPT calling the dialog
{
Object dlg = Alloc(myUI).Init()
dlg.pose()
}
You can also 'attach' information directly to the checkbox. Checkboxes are - as all dialog items - really just specific TagGroup objects to which you can add whatever you like. In the example below, I'm adding an additional tag with a random number:
class myUI : UIframe
{
void generalFunction( object self , tagGroup checkTg )
{
// checkTg is the taggroup of the checkbox which fired the method.
// Use its Title to get back the running value!
string label = checkTg.DLGGetTitle()
Result( "\n label of checkbox:" + label )
number rnd
if ( checkTG.TagGroupGetTagAsNumber( "Random NR", rnd ) )
{
Result( "\n Random number:" + rnd )
}
}
TagGroup CreateCheckboxes( object self )
{
TagGroup checkboxGroup = DLGCreateGroup()
for ( number i = 0; I < 5 ; i++ )
{
TagGroup checkbox = DLGCreateCheckBox( "C" + I , 0 , "generalFunction" )
checkbox.TagGroupSetTagAsNumber( "Random NR", Random() )
checkboxGroup.DLGAddElement( checkbox )
}
return checkboxGroup
}
TagGroup CreateDLGTags( object self )
{
TagGroup dlg, dlgitems
dlg = DLGCreateDialog( "Test" , dlgitems )
dlgitems.DLGAddElement( self.CreateCheckboxes() )
return dlg
}
object Init( object self )
{
return self.super.init( self.CreateDLGTags() )
}
}
// MAIN SCRIPT calling the dialog
{
Object dlg=Alloc(myUI).Init()
dlg.pose()
}
TestNG generates an emailable report. I have seen that this report can be customized by using Listeners. But could not get what i wanted. My requirement is to include extra details in the summary section of this report. I want to be able to add may be a new table or extra columns to show the environment details of the test execution.
Trying to attach a screenshot but apparently missing something and it does not come up.
That is what I have on my framework. I'll try to explain it (sorry my English)
Copy ReporterListenerAdapter.java and rename as MyReporterListenerAdapter.java, put it on your java project (/listener folder for example)
public class MyReporterListenerAdapter implements IReporter {
public void generateReport(List<XmlSuite> xml, List<ISuite> suites, String outdir) {}
}
Next, copy ReporterListener.java and rename as MyReporterListener.java
Too much code to paste here, but on createWriter function change the report name. For example: "emailable-MyFramework-report".
MyReporterListener.java
public class MyReporterListener extends MyReporterListenerAdapter {
private static final Logger L = Logger.getLogger(MyReporterListener.class);
// ~ Instance fields ------------------------------------------------------
private PrintWriter m_out;
private int m_row;
private Integer m_testIndex;
private int m_methodIndex;
private Scanner scanner;
// ~ Methods --------------------------------------------------------------
/** Creates summary of the run */
#Override
public void generateReport(List<XmlSuite> xml, List<ISuite> suites,
String outdir) {
try {
m_out = createWriter(outdir);
} catch (IOException e) {
L.error("output file", e);
return;
}
startHtml(m_out);
generateSuiteSummaryReport(suites);
generateMethodSummaryReport(suites);
generateMethodDetailReport(suites);
endHtml(m_out);
m_out.flush();
m_out.close();
}
protected PrintWriter createWriter(String outdir) throws IOException {
java.util.Date now = new Date();
new File(outdir).mkdirs();
return new PrintWriter(new BufferedWriter(new FileWriter(new File(
outdir, "emailable-FON-report"
+ DateFunctions.dateToDayAndTimeForFileName(now)
+ ".html"))));
}
/**
* Creates a table showing the highlights of each test method with links to
* the method details
*/
protected void generateMethodSummaryReport(List<ISuite> suites) {
m_methodIndex = 0;
startResultSummaryTable("methodOverview");
int testIndex = 1;
for (ISuite suite : suites) {
if (suites.size() > 1) {
titleRow(suite.getName(), 5);
}
Map<String, ISuiteResult> r = suite.getResults();
for (ISuiteResult r2 : r.values()) {
ITestContext testContext = r2.getTestContext();
String testName = testContext.getName();
m_testIndex = testIndex;
resultSummary(suite, testContext.getFailedConfigurations(),
testName, "failed", " (configuration methods)");
resultSummary(suite, testContext.getFailedTests(), testName,
"failed", "");
resultSummary(suite, testContext.getSkippedConfigurations(),
testName, "skipped", " (configuration methods)");
resultSummary(suite, testContext.getSkippedTests(), testName,
"skipped", "");
resultSummary(suite, testContext.getPassedTests(), testName,
"passed", "");
testIndex++;
}
}
m_out.println("</table>");
}
/** Creates a section showing known results for each method */
protected void generateMethodDetailReport(List<ISuite> suites) {
m_methodIndex = 0;
for (ISuite suite : suites) {
Map<String, ISuiteResult> r = suite.getResults();
for (ISuiteResult r2 : r.values()) {
ITestContext testContext = r2.getTestContext();
if (r.values().size() > 0) {
m_out.println("<h1>" + testContext.getName() + "</h1>");
}
resultDetail(testContext.getFailedConfigurations());
resultDetail(testContext.getFailedTests());
resultDetail(testContext.getSkippedConfigurations());
resultDetail(testContext.getSkippedTests());
resultDetail(testContext.getPassedTests());
}
}
}
/**
* #param tests
*/
private void resultSummary(ISuite suite, IResultMap tests, String testname,
String style, String details) {
if (tests.getAllResults().size() > 0) {
StringBuffer buff = new StringBuffer();
String lastClassName = "";
int mq = 0;
int cq = 0;
for (ITestNGMethod method : getMethodSet(tests, suite)) {
m_row += 1;
m_methodIndex += 1;
ITestClass testClass = method.getTestClass();
String className = testClass.getName();
if (mq == 0) {
String id = (m_testIndex == null ? null : "t"
+ Integer.toString(m_testIndex));
titleRow(testname + " — " + style + details, 5, id);
m_testIndex = null;
}
if (!className.equalsIgnoreCase(lastClassName)) {
if (mq > 0) {
cq += 1;
m_out.print("<tr class=\"" + style
+ (cq % 2 == 0 ? "even" : "odd") + "\">"
+ "<td");
if (mq > 1) {
m_out.print(" rowspan=\"" + mq + "\"");
}
m_out.println(">" + lastClassName + "</td>" + buff);
}
mq = 0;
buff.setLength(0);
lastClassName = className;
}
Set<ITestResult> resultSet = tests.getResults(method);
long end = Long.MIN_VALUE;
long start = Long.MAX_VALUE;
for (ITestResult testResult : tests.getResults(method)) {
if (testResult.getEndMillis() > end) {
end = testResult.getEndMillis();
}
if (testResult.getStartMillis() < start) {
start = testResult.getStartMillis();
}
}
mq += 1;
if (mq > 1) {
buff.append("<tr class=\"" + style
+ (cq % 2 == 0 ? "odd" : "even") + "\">");
}
String description = method.getDescription();
String testInstanceName = resultSet
.toArray(new ITestResult[] {})[0].getTestName();
buff.append("<td><a href=\"#m"
+ m_methodIndex
+ "\">"
+ qualifiedName(method)
+ " "
+ (description != null && description.length() > 0 ? "(\""
+ description + "\")"
: "")
+ "</a>"
+ (null == testInstanceName ? "" : "<br>("
+ testInstanceName + ")") + "</td>"
+ "<td class=\"numi\">" + resultSet.size() + "</td>"
+ "<td>" + start + "</td>" + "<td class=\"numi\">"
+ (end - start) + "</td>" + "</tr>");
}
if (mq > 0) {
cq += 1;
m_out.print("<tr class=\"" + style
+ (cq % 2 == 0 ? "even" : "odd") + "\">" + "<td");
if (mq > 1) {
m_out.print(" rowspan=\"" + mq + "\"");
}
m_out.println(">" + lastClassName + "</td>" + buff);
}
}
}
/** Starts and defines columns result summary table */
private void startResultSummaryTable(String style) {
tableStart(style, "summary");
m_out.println("<tr><th>Class</th>"
+ "<th>Method</th><th># of<br/>Scenarios</th><th>Start</th><th>Time<br/>(ms)</th></tr>");
m_row = 0;
}
private String qualifiedName(ITestNGMethod method) {
StringBuilder addon = new StringBuilder();
String[] groups = method.getGroups();
int length = groups.length;
if (length > 0 && !"basic".equalsIgnoreCase(groups[0])) {
addon.append("(");
for (int i = 0; i < length; i++) {
if (i > 0) {
addon.append(", ");
}
addon.append(groups[i]);
}
addon.append(")");
}
return "<b>" + method.getMethodName() + "</b> " + addon;
}
private void resultDetail(IResultMap tests) {
for (ITestResult result : tests.getAllResults()) {
ITestNGMethod method = result.getMethod();
m_methodIndex++;
String cname = method.getTestClass().getName();
m_out.println("<h2 id=\"m" + m_methodIndex + "\">" + cname + ":"
+ method.getMethodName() + "</h2>");
Set<ITestResult> resultSet = tests.getResults(method);
generateForResult(result, method, resultSet.size());
m_out.println("<p class=\"totop\">back to summary</p>");
}
}
/**
* Write the first line of the stack trace
*
* #param tests
*/
private void getShortException(IResultMap tests) {
for (ITestResult result : tests.getAllResults()) {
m_methodIndex++;
Throwable exception = result.getThrowable();
List<String> msgs = Reporter.getOutput(result);
boolean hasReporterOutput = msgs.size() > 0;
boolean hasThrowable = exception != null;
if (hasThrowable) {
boolean wantsMinimalOutput = result.getStatus() == ITestResult.SUCCESS;
if (hasReporterOutput) {
m_out.print("<h3>"
+ (wantsMinimalOutput ? "Expected Exception"
: "Failure") + "</h3>");
}
// Getting first line of the stack trace
String str = Utils.stackTrace(exception, true)[0];
scanner = new Scanner(str);
String firstLine = scanner.nextLine();
m_out.println(firstLine);
}
}
}
/**
* Write all parameters
*
* #param tests
*/
private void getParameters(IResultMap tests) {
for (ITestResult result : tests.getAllResults()) {
m_methodIndex++;
Object[] parameters = result.getParameters();
boolean hasParameters = parameters != null && parameters.length > 0;
if (hasParameters) {
for (Object p : parameters) {
m_out.println(Utils.escapeHtml(Utils.toString(p)) + " | ");
}
}
}
}
private void generateForResult(ITestResult ans, ITestNGMethod method,
int resultSetSize) {
Object[] parameters = ans.getParameters();
boolean hasParameters = parameters != null && parameters.length > 0;
if (hasParameters) {
tableStart("result", null);
m_out.print("<tr class=\"param\">");
for (int x = 1; x <= parameters.length; x++) {
m_out.print("<th>Param." + x + "</th>");
}
m_out.println("</tr>");
m_out.print("<tr class=\"param stripe\">");
for (Object p : parameters) {
m_out.println("<td>" + Utils.escapeHtml(Utils.toString(p))
+ "</td>");
}
m_out.println("</tr>");
}
List<String> msgs = Reporter.getOutput(ans);
boolean hasReporterOutput = msgs.size() > 0;
Throwable exception = ans.getThrowable();
boolean hasThrowable = exception != null;
if (hasReporterOutput || hasThrowable) {
if (hasParameters) {
m_out.print("<tr><td");
if (parameters.length > 1) {
m_out.print(" colspan=\"" + parameters.length + "\"");
}
m_out.println(">");
} else {
m_out.println("<div>");
}
if (hasReporterOutput) {
if (hasThrowable) {
m_out.println("<h3>Test Messages</h3>");
}
for (String line : msgs) {
m_out.println(line + "<br/>");
}
}
if (hasThrowable) {
boolean wantsMinimalOutput = ans.getStatus() == ITestResult.SUCCESS;
if (hasReporterOutput) {
m_out.println("<h3>"
+ (wantsMinimalOutput ? "Expected Exception"
: "Failure") + "</h3>");
}
generateExceptionReport(exception, method);
}
if (hasParameters) {
m_out.println("</td></tr>");
} else {
m_out.println("</div>");
}
}
if (hasParameters) {
m_out.println("</table>");
}
}
protected void generateExceptionReport(Throwable exception,
ITestNGMethod method) {
m_out.print("<div class=\"stacktrace\">");
m_out.print(Utils.stackTrace(exception, true)[0]);
m_out.println("</div>");
}
/**
* Since the methods will be sorted chronologically, we want to return the
* ITestNGMethod from the invoked methods.
*/
private Collection<ITestNGMethod> getMethodSet(IResultMap tests,
ISuite suite) {
List<IInvokedMethod> r = Lists.newArrayList();
List<IInvokedMethod> invokedMethods = suite.getAllInvokedMethods();
for (IInvokedMethod im : invokedMethods) {
if (tests.getAllMethods().contains(im.getTestMethod())) {
r.add(im);
}
}
Arrays.sort(r.toArray(new IInvokedMethod[r.size()]), new TestSorter());
List<ITestNGMethod> result = Lists.newArrayList();
// Add all the invoked methods
for (IInvokedMethod m : r) {
result.add(m.getTestMethod());
}
// Add all the methods that weren't invoked (e.g. skipped) that we
// haven't added yet
for (ITestNGMethod m : tests.getAllMethods()) {
if (!result.contains(m)) {
result.add(m);
}
}
return result;
}
#SuppressWarnings("unused")
public void generateSuiteSummaryReport(List<ISuite> suites) {
tableStart("testOverview", null);
m_out.print("<tr>");
tableColumnStart("Test");
tableColumnStart("Methods<br/>Passed");
tableColumnStart("Scenarios<br/>Passed");
tableColumnStart("# skipped");
tableColumnStart("# failed");
tableColumnStart("Error messages");
tableColumnStart("Parameters");
tableColumnStart("Start<br/>Time");
tableColumnStart("End<br/>Time");
tableColumnStart("Total<br/>Time");
tableColumnStart("Included<br/>Groups");
tableColumnStart("Excluded<br/>Groups");
m_out.println("</tr>");
NumberFormat formatter = new DecimalFormat("#,##0.0");
int qty_tests = 0;
int qty_pass_m = 0;
int qty_pass_s = 0;
int qty_skip = 0;
int qty_fail = 0;
long time_start = Long.MAX_VALUE;
long time_end = Long.MIN_VALUE;
m_testIndex = 1;
for (ISuite suite : suites) {
if (suites.size() > 1) {
titleRow(suite.getName(), 8);
}
Map<String, ISuiteResult> tests = suite.getResults();
for (ISuiteResult r : tests.values()) {
qty_tests += 1;
ITestContext overview = r.getTestContext();
startSummaryRow(overview.getName());
int q = getMethodSet(overview.getPassedTests(), suite).size();
qty_pass_m += q;
summaryCell(q, Integer.MAX_VALUE);
q = overview.getPassedTests().size();
qty_pass_s += q;
summaryCell(q, Integer.MAX_VALUE);
q = getMethodSet(overview.getSkippedTests(), suite).size();
qty_skip += q;
summaryCell(q, 0);
q = getMethodSet(overview.getFailedTests(), suite).size();
qty_fail += q;
summaryCell(q, 0);
// NEW
// Insert error found
m_out.print("<td class=\"numi" + (true ? "" : "_attn") + "\">");
getShortException(overview.getFailedTests());
getShortException(overview.getSkippedTests());
m_out.println("</td>");
// NEW
// Add parameters for each test case (failed or passed)
m_out.print("<td class=\"numi" + (true ? "" : "_attn") + "\">");
// Write OS and Browser
// m_out.println(suite.getParameter("os").substring(0, 3) +
// " | "
// + suite.getParameter("browser").substring(0, 3) + " | ");
getParameters(overview.getFailedTests());
getParameters(overview.getPassedTests());
getParameters(overview.getSkippedTests());
m_out.println("</td>");
// NEW
summaryCell(
DateFunctions.dateToDayAndTime(overview.getStartDate()),
true);
m_out.println("</td>");
summaryCell(
DateFunctions.dateToDayAndTime(overview.getEndDate()),
true);
m_out.println("</td>");
time_start = Math.min(overview.getStartDate().getTime(),
time_start);
time_end = Math.max(overview.getEndDate().getTime(), time_end);
summaryCell(
formatter.format((overview.getEndDate().getTime() - overview
.getStartDate().getTime()) / 1000.)
+ " seconds", true);
summaryCell(overview.getIncludedGroups());
summaryCell(overview.getExcludedGroups());
m_out.println("</tr>");
m_testIndex++;
}
}
if (qty_tests > 1) {
m_out.println("<tr class=\"total\"><td>Total</td>");
summaryCell(qty_pass_m, Integer.MAX_VALUE);
summaryCell(qty_pass_s, Integer.MAX_VALUE);
summaryCell(qty_skip, 0);
summaryCell(qty_fail, 0);
summaryCell(" ", true);
summaryCell(" ", true);
summaryCell(" ", true);
summaryCell(" ", true);
summaryCell(
formatter.format(((time_end - time_start) / 1000.) / 60.)
+ " minutes", true);
m_out.println("<td colspan=\"3\"> </td></tr>");
}
m_out.println("</table>");
}
private void summaryCell(String[] val) {
StringBuffer b = new StringBuffer();
for (String v : val) {
b.append(v + " ");
}
summaryCell(b.toString(), true);
}
private void summaryCell(String v, boolean isgood) {
m_out.print("<td class=\"numi" + (isgood ? "" : "_attn") + "\">" + v
+ "</td>");
}
private void startSummaryRow(String label) {
m_row += 1;
m_out.print("<tr"
+ (m_row % 2 == 0 ? " class=\"stripe\"" : "")
+ "><td style=\"text-align:left;padding-right:2em\"><a href=\"#t"
+ m_testIndex + "\">" + label + "</a>" + "</td>");
}
private void summaryCell(int v, int maxexpected) {
summaryCell(String.valueOf(v), v <= maxexpected);
}
private void tableStart(String cssclass, String id) {
m_out.println("<table cellspacing=\"0\" cellpadding=\"0\""
+ (cssclass != null ? " class=\"" + cssclass + "\""
: " style=\"padding-bottom:2em\"")
+ (id != null ? " id=\"" + id + "\"" : "") + ">");
m_row = 0;
}
private void tableColumnStart(String label) {
m_out.print("<th>" + label + "</th>");
}
private void titleRow(String label, int cq) {
titleRow(label, cq, null);
}
private void titleRow(String label, int cq, String id) {
m_out.print("<tr");
if (id != null) {
m_out.print(" id=\"" + id + "\"");
}
m_out.println("><th colspan=\"" + cq + "\">" + label + "</th></tr>");
m_row = 0;
}
/** Starts HTML stream */
protected void startHtml(PrintWriter out) {
out.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">");
out.println("<html xmlns=\"http://www.w3.org/1999/xhtml\">");
out.println("<head>");
out.println("<title>Hector Flores - TestNG Report</title>");
out.println("<style type=\"text/css\">");
out.println("table {margin-bottom:10px;border-collapse:collapse;empty-cells:show}");
out.println("td,th {border:1px solid #009;padding:.25em .5em}");
out.println(".result th {vertical-align:bottom}");
out.println(".param th {padding-left:1em;padding-right:1em}");
out.println(".param td {padding-left:.5em;padding-right:2em}");
out.println(".stripe td,.stripe th {background-color: #E6EBF9}");
out.println(".numi,.numi_attn {text-align:right}");
out.println(".total td {font-weight:bold}");
out.println(".passedodd td {background-color: #0A0}");
out.println(".passedeven td {background-color: #3F3}");
out.println(".skippedodd td {background-color: #CCC}");
out.println(".skippedodd td {background-color: #DDD}");
out.println(".failedodd td,.numi_attn {background-color: #F33}");
out.println(".failedeven td,.stripe .numi_attn {background-color: #D00}");
out.println(".stacktrace {white-space:pre;font-family:monospace}");
out.println(".totop {font-size:85%;text-align:center;border-bottom:2px solid #000}");
out.println("</style>");
out.println("</head>");
out.println("<body>");
}
/** Finishes HTML stream */
protected void endHtml(PrintWriter out) {
out.println("<center> Report customized by Hector Flores [hectorfb#gmail.com] </center>");
out.println("</body></html>");
}
// ~ Inner Classes --------------------------------------------------------
/** Arranges methods by classname and method name */
private class TestSorter implements Comparator<IInvokedMethod> {
// ~ Methods
// -------------------------------------------------------------
/** Arranges methods by classname and method name */
#Override
public int compare(IInvokedMethod o1, IInvokedMethod o2) {
// System.out.println("Comparing " + o1.getMethodName() + " " +
// o1.getDate()
// + " and " + o2.getMethodName() + " " + o2.getDate());
return (int) (o1.getDate() - o2.getDate());
// int r = ((T) o1).getTestClass().getName().compareTo(((T)
// o2).getTestClass().getName());
// if (r == 0) {
// r = ((T) o1).getMethodName().compareTo(((T) o2).getMethodName());
// }
// return r;
}
}
}
With those steps you already have your listener ready to listen.
How to call it?
If you use testng.xml add the following lines:
<listeners>
<listener class-name='[your_class_path].MyReporterListener'/>
</listeners>
If you run your tests from a java class, add the following lines:
private final static MyReporterListener frl = new MyReporterListener();
TestNG testng = new TestNG();
testng.addListener(frl);
With those steps, when you execute your tests you'll have two emailable reports, customized and original.
Now it's time to pimp your report.
In my case I had to add error messages, parameters and times (start and end), because it's very useful if you want to paste on an excel file.
My customized report:
You have to mainly modify generateSuiteSummaryReport(List suites) function.
Play with that and ask me if you have any problem.
Make a you CSS and embed it in EmailableReporter.class. This class file can be found in TestNG folder> org.testNg.reporters> EmailableReporter.class.
There you can edit the Style of the TestNG report HTML file which starts with
protected void startHtml(PrintWriter out)
Better you can use extent report html reporting library jar file. However i am using extentreport1.4.jar jar file. So you will get summary details at right corner of your report
Using the above code, I am facing the below null pointer issue, HTML report works fine when fewer tests are included in testng.xml, but when I include more tests, the HTML report does not get generated and the below exception is thrown
[TestNG] Reporter report.MyListener#60e07aed failed
java.lang.NullPointerException: Cannot invoke "String.length()" because "s" is null
at java.base/java.io.StringReader.<init>(StringReader.java:51)
at java.base/java.util.Scanner.<init>(Scanner.java:766)
at report.MyListener.getShortException(MyListener.java:294)
at report.MyListener.generateSuiteSummaryReport(MyListener.java:473)
at report.MyListener.generateReport(MyListener.java:72)
at org.testng.TestNG.generateReports(TestNG.java:1093)
at org.testng.TestNG.run(TestNG.java:1036)
at org.testng.remote.AbstractRemoteTestNG.run(AbstractRemoteTestNG.java:115)
at org.testng.remote.RemoteTestNG.initAndRun(RemoteTestNG.java:251)
at org.testng.remote.RemoteTestNG.main(RemoteTestNG.java:77)
To fix the above added a null check to avoid Null Pointer at line 295
if(str!=null) {
scanner = new Scanner(str);
String firstLine = scanner.nextLine()+"<br>";
m_out.println(firstLine);
}
I use Json.Net.
When I serialize a Department2 object and WriteJson() is invoked I want it to be recursively invoked with each of the Telephone2 objects like I do in ReadJson().
How do I do that?
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
public interface ISimpleDatabag
{
string Databag { get; set; }
}
[JsonConverter(typeof(JsonDataBagCreationConverter<Department2>))]
public class Department2
{
public Telephone2[] Phones { get; set; }
}
[JsonConverter(typeof(JsonDataBagCreationConverter<Telephone2>))]
public class Telephone2
{
public string Name { get; set; }
public string AreaCode { get; set; }
public string Number { get; set; }
}
public class JsonDataBagCreationConverter<T> : JsonConverter where T : new()
{
// Json.Net version 4.5.7.15008
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// When I serialize Department and this function is invoked
// I want it to recursively invoke WriteJson with each of the Telephone objects
// Like I do in ReadJson
// How do I do that?
T t = (T)value;
serializer.Serialize(writer, t.GetType());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jsonObject = JObject.Load(reader);
var target = Create(objectType, jsonObject);
serializer.Populate(jsonObject.CreateReader(), target); // Will call this function recursively for any objects that have JsonDataBagCreationConverter as attribute
return target;
}
protected T Create(Type objectType, JObject jsonObject)
{
return new T();
}
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
}
private void Form1_Load(object sender, EventArgs e)
{
string jsonInput = "{\"Name\": \"Seek4\" , \"CustomDepartmentData\": \"This is custom department data\", \"Phones\":[ {\"Name\": \"A\", \"AreaCode\":444, \"Number\":11111111} ,{\"Name\": \"B\", \"AreaCode\":555, \"Number\":987987987}, {\"Name\": \"C\", \"AreaCode\":222, \"Number\":123123123, \"CustomPhoneData\": \"This is custom phone data\"} ] }";
Department2 objDepartment2 = JsonConvert.DeserializeObject<Department2>(jsonInput); // Yes, it works well
Array.Reverse(objDepartment2.Phones);
string jsonNoDatabag = JsonConvert.SerializeObject(objDepartment2);
}
I ended up controlling the entire process myself, using this huge (not refactored) function.
I basically investigate each of the properties of the object to serialize and then serialize it property by property.
Then I can do custom things on each property
/// <summary>
/// Serializes an object by merging its current values into its databag and returns the databag
/// </summary>
/// <param name="objectToSerialize"></param>
/// <returns>the current values merged into the original databag</returns>
/// <remarks>Jan Nielsen, 01-10-2012</remarks>
internal static string SerializeObjectToDatabag(object objectToSerialize)
{
// You have to do it property by property instead of just serializing the entire object and merge it into the original
// because the object might contain lists of objects with custom data and these list might have been sorted differently from when they were loaded
// So you cannot merge them properly unless you do it on a per listitem basis.
// Which is what I do here.
try
{
if (objectToSerialize == null) // If you ie serialize an empty object in an array
{
return null;
}
string updatedDatabag = "";
bool isIDataBag = objectToSerialize is IDataBag;
if (isIDataBag)
{
updatedDatabag = ((IDataBag)objectToSerialize).Data == null ? "" : ((IDataBag)objectToSerialize).Data.ToString();
// updatedDatabag = ((IDataBag)objectToSerialize).Data.ToString(); // Save original data in a local variable. This is the one we will merge new values into
}
string result = "";
// Now iterate through the objects properties
// Possible properties:
// Simple types: string, int, bool etc: their current value should be overwritten in the databag
// types that implement IDatabag: they should be sent to this function recursively so their possible customdata is not overwritten
// but instead their simple values are merged into their own databag. Then the result of this single property merge is overwritten in the outer objects databag
// Types that are not simple and don't implement IDatabag but have properties that implement IDatabag
// types that are not simple and don't implement IDatabag and don't have any properties in any depth that implement IDatabag: They are overwritten in the databag
// Types that are arrays:
// If the types in the array are simple types (string, bool etc) the entire array property is overwritten in the databag
// If the types in the array implement IDatabag each object is sent recursively to this function and their databag is updated via merge
// Then the entire array is overwritten in the outer objects databag
// Types that are generic list are treated like arrays
var properties = objectToSerialize.GetType().GetProperties();
// In order to be able to deserialize abstract classes and interfaces, we need to serialize the classname with the class
// the deserializer recognizes the word $type followed by a type, when its is invoked with a serializerSettings of
// serializerSettings.TypeNameHandling = TypeNameHandling.Objects;
string name = objectToSerialize.GetType().AssemblyQualifiedName;
string shortName = RemoveAssemblyDetails(name);
bool addedType = false;
foreach (var propertyInfo in properties)
{
if (propertyInfo.Name.ToLower() != "data") // Just skip Databag. Databag is not a "real" property but the contents of all the properties when the object was loaded + possible custom data
{
if (!addedType)
{
string jsonSingleProperty = "{ " + ToCustomJson("$type") + " : " + ToCustomJson(shortName) + " }";
// Merge the current value (jsonSingleProperty) into the databag (that might already have been updated with the values of other properties)
// and update the current result with the new values. Ie "Name" : "Seek4" is updated to "Name" : "Seek4Cars" in the databag
// and the function will now use the updated databag to merge the other properties into
updatedDatabag = MergeDefault(jsonSingleProperty, updatedDatabag, true);
addedType = true;
}
// propertyInfo.Name.ToLower().Contains("struct")
var value = propertyInfo.GetValue(objectToSerialize, null); // This gets the value of the specified property in the current object
isIDataBag = value is IDataBag; // Update for the current object. Note that ie an array of IDatabag will return false here, because array is not IsimpleDatabag
// Basically we should just check if the property implements IDatabag
// But the simpletype check is faster because I don't have to check for the interfaces on ie a string, int etc.
// This branch takes care of 3 cases:
// 1) it is a simple type, ie int
// 2) value is null
// 3) it is an array with a value of null
// If an array with values enters this branch of code the values of the array will be appended, overwritten
// Therefore arrays are treated below in a special case. Unless they are null
// GeneralFunctions.IsExtendedSimpleType_AllTypes(propertyInfo.PropertyType) returns true on ie string[], but only arrays with a value of null should be handled here
// This first check originally just checked for simple types
// Then it became extended simple types ie non-simple types that only contains simple types ie List<int,int>
// But not arrays that must be handled separately
// Then it also handled null values
// And then a special case was made for arrays that are null
if ((GeneralFunctions.IsExtendedSimpleType_AllTypes(propertyInfo.PropertyType) || value == null) && (!propertyInfo.PropertyType.IsArray || (propertyInfo.PropertyType.IsArray && value == null)))
{
// You have to merge even though it is default value.
// If you have ie a bool that has an initial value of true and you deliberately sets it to false
// You want the defaultvalue of false to be merged into the json.
string jsonSingleProperty = "{" + ToCustomJson(propertyInfo.Name) + " : " + ToCustomJson(value) + "}"; // ie {"Name" : "Seek4Cars"}
// Merge the current value (jsonSingleProperty) into the databag (that might already have been updated with the values of other properties)
// and update the current result with the new values. Ie "Name" : "Seek4" is updated to "Name" : "Seek4Cars" in the databag
// and the function will now use the updated databag to merge the other properties into
updatedDatabag = MergeDefault(jsonSingleProperty, updatedDatabag, true);
continue;
}
if (isIDataBag) // ie PhoneSingle. A single property of type IDataBag
{
// Invoke recursively
// First check if this is an object with all null values
bool allPropertiesAreNull = true; // Maybe this should in the future be expanded with a check on if the property has its default value ie an int property with a value of 0
foreach (var propertyInfoLocal in value.GetType().GetProperties())
{
var valueLocal = propertyInfoLocal.GetValue(value, null);
if (valueLocal != null)
{
allPropertiesAreNull = false;
break;
}
}
var testjson = "";
if (allPropertiesAreNull)
{
result = "{" + ToCustomJson(propertyInfo.Name) + " : " + " { } }";
}
else
{
testjson = ToCustomJson(value);
result = "{" + ToCustomJson(propertyInfo.Name) + " : " + SerializeObjectToDatabag(value) + "}";
}
updatedDatabag = MergeDefault(result, updatedDatabag, true);
continue;
}
bool containsIDataBag = CheckForDatabagInterfaces.ImplementsInterface(propertyInfo.PropertyType, "idatabag"); // Check if anything inside the property implements IDatabag ie an array of IDatabag
if (containsIDataBag)
{
// Check if it is somekind of generic list (List<T>, Dictionary<T,T) etc) and if it is a type of ignoreTypes ie List<entity>)
if (value.GetType().IsGenericType && value.GetType().GetGenericArguments().Length > 0)
{
string listValuesAsJson = "";
if (value is IEnumerable)
{
listValuesAsJson += "{ " + ToCustomJson(propertyInfo.Name) + " : [";
bool containsItems = false;
foreach (var element in (IEnumerable)value)
{
containsItems = true;
var current = SerializeObjectToDatabag(element);
if (current != null) // If you serialize an empty array element it is null
{
listValuesAsJson += current + ", "; // Add , between each element
}
}
if (containsItems)
{
listValuesAsJson = listValuesAsJson.Substring(0, listValuesAsJson.Length - 2) + "] }"; // remove last , and add ending ] for the array and add a } because this property is flowing in the free
}
else // No items in value
{
listValuesAsJson += "] }"; // add ending ] for the array and add a } because this property is flowing in the free
}
}
else // A single, generic KeyValuePair property
{
listValuesAsJson += "{ " + ToCustomJson(propertyInfo.Name) + " : ";
listValuesAsJson += SerializeObjectToDatabag(value);
listValuesAsJson += " }";
}
updatedDatabag = MergeDefault(listValuesAsJson, updatedDatabag, false);
}
else if (value.GetType().IsArray)
{
string arrayValuesAsJson = "{ " + ToCustomJson(propertyInfo.Name) + " : [";
bool containsItems = false;
foreach (var element in (Array)value)
{
// Treat them the same way you treat any other object
var current = SerializeObjectToDatabag(element);
if (current != null) // If you serialize an empty array element it is null
{
containsItems = true;
arrayValuesAsJson += current + ", ";
}
}
if (containsItems)
{
arrayValuesAsJson = arrayValuesAsJson.Substring(0, arrayValuesAsJson.Length - 2) + "] }"; // remove last , and add ending ] for the array and add a } because this property is flowing in the free
}
else // No items in value
{
arrayValuesAsJson += "] }"; // add ending ] for the array and add a } because this property is flowing in the free
}
updatedDatabag = MergeDefault(arrayValuesAsJson, updatedDatabag, false);
}
else if ( value.GetType().BaseType != null && value.GetType().BaseType.FullName.ToLower().Contains("system.collections.objectmodel"))
{
// This branch was made specifically to take care of the Media collection of a Seek4.Entities.V2.Media.MediaCollection
var genericList = (IList)value;
int counter = genericList.Count;
string listAsJson = "{ " + ToCustomJson(propertyInfo.Name) + " : [";
if (counter == 0)
{
listAsJson += "] }"; // Ie { "Media": [] }
}
else
{
foreach (var obj in genericList)
{
var current = SerializeObjectToDatabag(obj);
listAsJson += current + ", ";
}
listAsJson = listAsJson.Substring(0, listAsJson.Length -2) + " ] }" ;
}
updatedDatabag = MergeDefault(listAsJson, updatedDatabag, true); // hvordan gør json.net dette med standard?
}
else // a single Non-IDatabag property that contains Idatabag properties
{
string tempResult = "{ " + ToCustomJson(propertyInfo.Name) + " : ";
tempResult += SerializeObjectToDatabag(value) + " }";
updatedDatabag = MergeDefault(tempResult, updatedDatabag, true);
}
}
else
{
if (value.GetType().IsArray) // This is an array of simple types so just overwrite
{
string arrayAsJson = "{ " + ToCustomJson(propertyInfo.Name) + " : ";
arrayAsJson += ToCustomJson(value) + "}";
updatedDatabag = MergeDefault(arrayAsJson, updatedDatabag, false);
}
else // ie an object that is not simpledatabag and doesn't contain simple databag
{
string jsonSingleProperty = "{" + ToCustomJson(propertyInfo.Name) + " : " + ToCustomJson(value) + "}";
updatedDatabag = MergeDefault(jsonSingleProperty, updatedDatabag, true);
}
}
}
}
return updatedDatabag;
}
catch (Exception ex)
{
string message = ex.Message;
string stack = ex.StackTrace;
throw;
}
}
internal static string ToCustomJson(object objectToConvertToJson)
{
try
{
// Distinguished from Mongodb.Bson.ToJson() extensionmethod by Custom name
JsonSerializerSettings serializerSettings = new JsonSerializerSettings();
serializerSettings.TypeNameHandling = TypeNameHandling.Objects; // Adds a $type on all objects which we need when it is abstract classes and interfaces
IgnoreDataMemberContractResolver contractResolver = new IgnoreDataMemberContractResolver(null, true, true);
serializerSettings.ContractResolver = contractResolver;
serializerSettings.DefaultValueHandling = DefaultValueHandling.Ignore;
IsoDateTimeConverter converter = new IsoDateTimeConverter();
serializerSettings.Converters.Add(converter);
string result = JsonConvert.SerializeObject(objectToConvertToJson, Formatting.None, serializerSettings);
return result;
}
catch (Exception ex)
{
throw new Exception("Error in ToCustomJson: " + ex.Message, ex);
}
}