diff options
Diffstat (limited to 'lib/common_test')
42 files changed, 7120 insertions, 848 deletions
diff --git a/lib/common_test/doc/src/Makefile b/lib/common_test/doc/src/Makefile index 2ec6952710..99161ce68a 100644 --- a/lib/common_test/doc/src/Makefile +++ b/lib/common_test/doc/src/Makefile @@ -1,7 +1,7 @@ # # %CopyrightBegin% # -# Copyright Ericsson AB 2003-2011. All Rights Reserved. +# Copyright Ericsson AB 2003-2012. All Rights Reserved. # # The contents of this file are subject to the Erlang Public License, # Version 1.1, (the "License"); you may not use this file except in @@ -46,7 +46,8 @@ CT_MODULES = \ ct_rpc \ ct_snmp \ unix_telnet \ - ct_slave + ct_slave \ + ct_netconfc CT_XML_FILES = $(CT_MODULES:=.xml) @@ -123,7 +124,7 @@ $(HTMLDIR)/%.gif: %.gif docs: pdf html man -$(CT_XML_FILES): +$(CT_XML_FILES): %.xml: ../../src/%.erl escript $(DOCGEN)/priv/bin/xml_from_edoc.escript -preprocess true -i $(XMERL_DIR)/include \ -i ../../../test_server/include -i ../../include \ -i ../../../../erts/lib/kernel/include -i ../../../../lib/kernel/include \ diff --git a/lib/common_test/doc/src/ct_hooks_chapter.xml b/lib/common_test/doc/src/ct_hooks_chapter.xml index 1dbb841fb0..014507c886 100644 --- a/lib/common_test/doc/src/ct_hooks_chapter.xml +++ b/lib/common_test/doc/src/ct_hooks_chapter.xml @@ -453,7 +453,7 @@ terminate(State) -> <cell>Captures all test results and outputs them as surefire XML into a file. The file which is created is by default called junit_report.xml. The name can be by setting the path option for this hook. e.g. - <code>-ct_hooks cth_surefix [{path,"/tmp/report.xml"}]</code> + <code>-ct_hooks cth_surefire [{path,"/tmp/report.xml"}]</code> Surefire XML can forinstance be used by Jenkins to display test results.</cell> </row> diff --git a/lib/common_test/doc/src/ref_man.xml b/lib/common_test/doc/src/ref_man.xml index a9fdef7359..6fede88434 100644 --- a/lib/common_test/doc/src/ref_man.xml +++ b/lib/common_test/doc/src/ref_man.xml @@ -4,7 +4,7 @@ <application xmlns:xi="http://www.w3.org/2001/XInclude"> <header> <copyright> - <year>2003</year><year>2011</year> + <year>2003</year><year>2012</year> <holder>Ericsson AB. All Rights Reserved.</holder> </copyright> <legalnotice> @@ -71,6 +71,7 @@ <xi:include href="ct_cover.xml"/> <xi:include href="ct_ftp.xml"/> <xi:include href="ct_ssh.xml"/> + <xi:include href="ct_netconfc.xml"/> <xi:include href="ct_rpc.xml"/> <xi:include href="ct_snmp.xml"/> <xi:include href="ct_telnet.xml"/> diff --git a/lib/common_test/doc/src/run_test_chapter.xml b/lib/common_test/doc/src/run_test_chapter.xml index 30486d3eec..bc0af63790 100644 --- a/lib/common_test/doc/src/run_test_chapter.xml +++ b/lib/common_test/doc/src/run_test_chapter.xml @@ -425,7 +425,7 @@ <p>Below is the test specification syntax. Test specifications can be used to run tests both in a single test host environment and in a distributed Common Test environment (Large Scale - Testing). The node parameters in the init term are only + Testing). The node parameters in the <c>init</c> term are only relevant in the latter (see the <seealso marker="ct_master_chapter#test_specifications">Large Scale Testing</seealso> chapter for information). For details on @@ -575,9 +575,9 @@ <item>Lastly, all suites for systems t3 are to be completely skipped and this should be explicitly noted in the log files.</item> </list> - <p>It is possible to specify initialization options for nodes defined in the - test specification. Currently, there are options to start the node and/or to - evaluate any function on the node. + <p>With the <c>init</c> term it's possible to specify initialization options + for nodes defined in the test specification. Currently, there are options + to start the node and/or to evaluate any function on the node. See the <seealso marker="ct_master_chapter#ct_slave">Automatic startup of the test target nodes</seealso> chapter for details.</p> <p>It is possible for the user to provide a test specification that diff --git a/lib/common_test/priv/Makefile.in b/lib/common_test/priv/Makefile.in index 8be6248e17..2f06f7df65 100644 --- a/lib/common_test/priv/Makefile.in +++ b/lib/common_test/priv/Makefile.in @@ -60,6 +60,7 @@ FILES = vts.tool SCRIPTS = IMAGES = tile1.jpg CSS = ct_default.css +JS = jquery-latest.js jquery.tablesorter.min.js # # Rules @@ -86,11 +87,11 @@ include $(ERL_TOP)/make/otp_release_targets.mk ifeq ($(XNIX),true) release_spec: opt $(INSTALL_DIR) "$(RELSYSDIR)/priv" - $(INSTALL_DATA) $(FILES) $(IMAGES) $(CSS) "$(RELSYSDIR)/priv" + $(INSTALL_DATA) $(FILES) $(IMAGES) $(CSS) $(JS) "$(RELSYSDIR)/priv" else release_spec: opt $(INSTALL_DIR) "$(RELSYSDIR)/priv" - $(INSTALL_DATA) $(FILES) $(IMAGES) $(CSS) "$(RELSYSDIR)/priv" + $(INSTALL_DATA) $(FILES) $(IMAGES) $(CSS) $(JS) "$(RELSYSDIR)/priv" endif release_docs_spec: @@ -107,6 +108,7 @@ else FILES = vts.tool IMAGES = tile1.jpg CSS = ct_default.css +JS = jquery-latest.js jquery.tablesorter.min.js # # Rules @@ -126,7 +128,7 @@ include $(ERL_TOP)/make/otp_release_targets.mk release_spec: opt $(INSTALL_DIR) "$(RELSYSDIR)/priv" - $(INSTALL_DATA) $(FILES) $(IMAGES) $(CSS) "$(RELSYSDIR)/priv" + $(INSTALL_DATA) $(FILES) $(IMAGES) $(CSS) $(JS) "$(RELSYSDIR)/priv" release_docs_spec: diff --git a/lib/common_test/priv/ct_default.css b/lib/common_test/priv/ct_default.css index 8ae6990cd8..1188f8f676 100644 --- a/lib/common_test/priv/ct_default.css +++ b/lib/common_test/priv/ct_default.css @@ -136,7 +136,18 @@ th { } thead th { - background: #2C5755; text-align: center; + background: #3F3F3F; color: #fff; + font-family: arial, sans-serif; font-size: 120%; + letter-spacing: -0.5px; + font-weight: bold; text-align: center; + padding-right: .5em; vertical-align: top; + text-decoration: underline; +} + +tfoot td { + font-family: arial, sans-serif; font-size: 110%; + letter-spacing: -0.5px; + font-weight: bold; } .odd td { @@ -167,10 +178,6 @@ th a, td a:active { color: #85ABD5; } -tfoot th, tfoot td { - background: #3F3F3F; color: #fff; -} - th + td { padding-left: .5em; } diff --git a/lib/common_test/priv/jquery-latest.js b/lib/common_test/priv/jquery-latest.js new file mode 100644 index 0000000000..ac7e7009dc --- /dev/null +++ b/lib/common_test/priv/jquery-latest.js @@ -0,0 +1,154 @@ +/*!
+ * jQuery JavaScript Library v1.4.2
+ * http://jquery.com/
+ *
+ * Copyright 2010, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ * Copyright 2010, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ *
+ * Date: Sat Feb 13 22:33:48 2010 -0500
+ */
+(function(A,w){function ma(){if(!c.isReady){try{s.documentElement.doScroll("left")}catch(a){setTimeout(ma,1);return}c.ready()}}function Qa(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function X(a,b,d,f,e,j){var i=a.length;if(typeof b==="object"){for(var o in b)X(a,o,b[o],f,e,d);return a}if(d!==w){f=!j&&f&&c.isFunction(d);for(o=0;o<i;o++)e(a[o],b,f?d.call(a[o],o,e(a[o],b)):d,j);return a}return i?
+e(a[0],b):w}function J(){return(new Date).getTime()}function Y(){return false}function Z(){return true}function na(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function oa(a){var b,d=[],f=[],e=arguments,j,i,o,k,n,r;i=c.data(this,"events");if(!(a.liveFired===this||!i||!i.live||a.button&&a.type==="click")){a.liveFired=this;var u=i.live.slice(0);for(k=0;k<u.length;k++){i=u[k];i.origType.replace(O,"")===a.type?f.push(i.selector):u.splice(k--,1)}j=c(a.target).closest(f,a.currentTarget);n=0;for(r=
+j.length;n<r;n++)for(k=0;k<u.length;k++){i=u[k];if(j[n].selector===i.selector){o=j[n].elem;f=null;if(i.preType==="mouseenter"||i.preType==="mouseleave")f=c(a.relatedTarget).closest(i.selector)[0];if(!f||f!==o)d.push({elem:o,handleObj:i})}}n=0;for(r=d.length;n<r;n++){j=d[n];a.currentTarget=j.elem;a.data=j.handleObj.data;a.handleObj=j.handleObj;if(j.handleObj.origHandler.apply(j.elem,e)===false){b=false;break}}return b}}function pa(a,b){return"live."+(a&&a!=="*"?a+".":"")+b.replace(/\./g,"`").replace(/ /g,
+"&")}function qa(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function ra(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var f=c.data(a[d++]),e=c.data(this,f);if(f=f&&f.events){delete e.handle;e.events={};for(var j in f)for(var i in f[j])c.event.add(this,j,f[j][i],f[j][i].data)}}})}function sa(a,b,d){var f,e,j;b=b&&b[0]?b[0].ownerDocument||b[0]:s;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&b===s&&!ta.test(a[0])&&(c.support.checkClone||!ua.test(a[0]))){e=
+true;if(j=c.fragments[a[0]])if(j!==1)f=j}if(!f){f=b.createDocumentFragment();c.clean(a,b,f,d)}if(e)c.fragments[a[0]]=j?f:1;return{fragment:f,cacheable:e}}function K(a,b){var d={};c.each(va.concat.apply([],va.slice(0,b)),function(){d[this]=a});return d}function wa(a){return"scrollTo"in a&&a.document?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var c=function(a,b){return new c.fn.init(a,b)},Ra=A.jQuery,Sa=A.$,s=A.document,T,Ta=/^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/,Ua=/^.[^:#\[\.,]*$/,Va=/\S/,
+Wa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Xa=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,P=navigator.userAgent,xa=false,Q=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,R=Array.prototype.slice,ya=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(a==="body"&&!b){this.context=s;this[0]=s.body;this.selector="body";this.length=1;return this}if(typeof a==="string")if((d=Ta.exec(a))&&
+(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:s;if(a=Xa.exec(a))if(c.isPlainObject(b)){a=[s.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=sa([d[1]],[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}return c.merge(this,a)}else{if(b=s.getElementById(d[2])){if(b.id!==d[2])return T.find(a);this.length=1;this[0]=b}this.context=s;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=s;a=s.getElementsByTagName(a);return c.merge(this,
+a)}else return!b||b.jquery?(b||T).find(a):c(b).find(a);else if(c.isFunction(a))return T.ready(a);if(a.selector!==w){this.selector=a.selector;this.context=a.context}return c.makeArray(a,this)},selector:"",jquery:"1.4.2",length:0,size:function(){return this.length},toArray:function(){return R.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){var f=c();c.isArray(a)?ba.apply(f,a):c.merge(f,a);f.prevObject=this;f.context=this.context;if(b===
+"find")f.selector=this.selector+(this.selector?" ":"")+d;else if(b)f.selector=this.selector+"."+b+"("+d+")";return f},each:function(a,b){return c.each(this,a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(s,c);else Q&&Q.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(R.apply(this,arguments),"slice",R.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this,
+function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice};c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,j,i,o;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b<d;b++)if((e=arguments[b])!=null)for(j in e){i=a[j];o=e[j];if(a!==o)if(f&&o&&(c.isPlainObject(o)||c.isArray(o))){i=i&&(c.isPlainObject(i)||
+c.isArray(i))?i:c.isArray(o)?[]:{};a[j]=c.extend(f,i,o)}else if(o!==w)a[j]=o}return a};c.extend({noConflict:function(a){A.$=Sa;if(a)A.jQuery=Ra;return c},isReady:false,ready:function(){if(!c.isReady){if(!s.body)return setTimeout(c.ready,13);c.isReady=true;if(Q){for(var a,b=0;a=Q[b++];)a.call(s,c);Q=null}c.fn.triggerHandler&&c(s).triggerHandler("ready")}},bindReady:function(){if(!xa){xa=true;if(s.readyState==="complete")return c.ready();if(s.addEventListener){s.addEventListener("DOMContentLoaded",
+L,false);A.addEventListener("load",c.ready,false)}else if(s.attachEvent){s.attachEvent("onreadystatechange",L);A.attachEvent("onload",c.ready);var a=false;try{a=A.frameElement==null}catch(b){}s.documentElement.doScroll&&a&&ma()}}},isFunction:function(a){return $.call(a)==="[object Function]"},isArray:function(a){return $.call(a)==="[object Array]"},isPlainObject:function(a){if(!a||$.call(a)!=="[object Object]"||a.nodeType||a.setInterval)return false;if(a.constructor&&!aa.call(a,"constructor")&&!aa.call(a.constructor.prototype,
+"isPrototypeOf"))return false;var b;for(b in a);return b===w||aa.call(a,b)},isEmptyObject:function(a){for(var b in a)return false;return true},error:function(a){throw a;},parseJSON:function(a){if(typeof a!=="string"||!a)return null;a=c.trim(a);if(/^[\],:{}\s]*$/.test(a.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return A.JSON&&A.JSON.parse?A.JSON.parse(a):(new Function("return "+
+a))();else c.error("Invalid JSON: "+a)},noop:function(){},globalEval:function(a){if(a&&Va.test(a)){var b=s.getElementsByTagName("head")[0]||s.documentElement,d=s.createElement("script");d.type="text/javascript";if(c.support.scriptEval)d.appendChild(s.createTextNode(a));else d.text=a;b.insertBefore(d,b.firstChild);b.removeChild(d)}},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,b,d){var f,e=0,j=a.length,i=j===w||c.isFunction(a);if(d)if(i)for(f in a){if(b.apply(a[f],
+d)===false)break}else for(;e<j;){if(b.apply(a[e++],d)===false)break}else if(i)for(f in a){if(b.call(a[f],f,a[f])===false)break}else for(d=a[0];e<j&&b.call(d,e,d)!==false;d=a[++e]);return a},trim:function(a){return(a||"").replace(Wa,"")},makeArray:function(a,b){b=b||[];if(a!=null)a.length==null||typeof a==="string"||c.isFunction(a)||typeof a!=="function"&&a.setInterval?ba.call(b,a):c.merge(b,a);return b},inArray:function(a,b){if(b.indexOf)return b.indexOf(a);for(var d=0,f=b.length;d<f;d++)if(b[d]===
+a)return d;return-1},merge:function(a,b){var d=a.length,f=0;if(typeof b.length==="number")for(var e=b.length;f<e;f++)a[d++]=b[f];else for(;b[f]!==w;)a[d++]=b[f++];a.length=d;return a},grep:function(a,b,d){for(var f=[],e=0,j=a.length;e<j;e++)!d!==!b(a[e],e)&&f.push(a[e]);return f},map:function(a,b,d){for(var f=[],e,j=0,i=a.length;j<i;j++){e=b(a[j],j,d);if(e!=null)f[f.length]=e}return f.concat.apply([],f)},guid:1,proxy:function(a,b,d){if(arguments.length===2)if(typeof b==="string"){d=a;a=d[b];b=w}else if(b&&
+!c.isFunction(b)){d=b;b=w}if(!b&&a)b=function(){return a.apply(d||this,arguments)};if(a)b.guid=a.guid=a.guid||b.guid||c.guid++;return b},uaMatch:function(a){a=a.toLowerCase();a=/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version)?[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||!/compatible/.test(a)&&/(mozilla)(?:.*? rv:([\w.]+))?/.exec(a)||[];return{browser:a[1]||"",version:a[2]||"0"}},browser:{}});P=c.uaMatch(P);if(P.browser){c.browser[P.browser]=true;c.browser.version=P.version}if(c.browser.webkit)c.browser.safari=
+true;if(ya)c.inArray=function(a,b){return ya.call(b,a)};T=c(s);if(s.addEventListener)L=function(){s.removeEventListener("DOMContentLoaded",L,false);c.ready()};else if(s.attachEvent)L=function(){if(s.readyState==="complete"){s.detachEvent("onreadystatechange",L);c.ready()}};(function(){c.support={};var a=s.documentElement,b=s.createElement("script"),d=s.createElement("div"),f="script"+J();d.style.display="none";d.innerHTML=" <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";
+var e=d.getElementsByTagName("*"),j=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!j)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(j.getAttribute("style")),hrefNormalized:j.getAttribute("href")==="/a",opacity:/^0.55$/.test(j.style.opacity),cssFloat:!!j.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:s.createElement("select").appendChild(s.createElement("option")).selected,
+parentNode:d.removeChild(d.appendChild(s.createElement("div"))).parentNode===null,deleteExpando:true,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};b.type="text/javascript";try{b.appendChild(s.createTextNode("window."+f+"=1;"))}catch(i){}a.insertBefore(b,a.firstChild);if(A[f]){c.support.scriptEval=true;delete A[f]}try{delete b.test}catch(o){c.support.deleteExpando=false}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function k(){c.support.noCloneEvent=
+false;d.detachEvent("onclick",k)});d.cloneNode(true).fireEvent("onclick")}d=s.createElement("div");d.innerHTML="<input type='radio' name='radiotest' checked='checked'/>";a=s.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var k=s.createElement("div");k.style.width=k.style.paddingLeft="1px";s.body.appendChild(k);c.boxModel=c.support.boxModel=k.offsetWidth===2;s.body.removeChild(k).style.display="none"});a=function(k){var n=
+s.createElement("div");k="on"+k;var r=k in n;if(!r){n.setAttribute(k,"return;");r=typeof n[k]==="function"}return r};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=j=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ya=0,za={};c.extend({cache:{},expando:G,noData:{embed:true,object:true,
+applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var f=a[G],e=c.cache;if(!f&&typeof b==="string"&&d===w)return null;f||(f=++Ya);if(typeof b==="object"){a[G]=f;e[f]=c.extend(true,{},b)}else if(!e[f]){a[G]=f;e[f]={}}a=e[f];if(d!==w)a[b]=d;return typeof b==="string"?a[b]:a}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{if(c.support.deleteExpando)delete a[c.expando];
+else a.removeAttribute&&a.removeAttribute(c.expando);delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===w){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===w&&this.length)f=c.data(this[0],a);return f===w&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this,
+a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b===
+w)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var Aa=/[\n\t]/g,ca=/\s+/,Za=/\r/g,$a=/href|src|style/,ab=/(button|input)/i,bb=/(button|input|object|select|textarea)/i,
+cb=/^(a|area)$/i,Ba=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(n){var r=c(this);r.addClass(a.call(this,n,r.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1)if(e.className){for(var j=" "+e.className+" ",
+i=e.className,o=0,k=b.length;o<k;o++)if(j.indexOf(" "+b[o]+" ")<0)i+=" "+b[o];e.className=c.trim(i)}else e.className=a}return this},removeClass:function(a){if(c.isFunction(a))return this.each(function(k){var n=c(this);n.removeClass(a.call(this,k,n.attr("class")))});if(a&&typeof a==="string"||a===w)for(var b=(a||"").split(ca),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1&&e.className)if(a){for(var j=(" "+e.className+" ").replace(Aa," "),i=0,o=b.length;i<o;i++)j=j.replace(" "+b[i]+" ",
+" ");e.className=c.trim(j)}else e.className=""}return this},toggleClass:function(a,b){var d=typeof a,f=typeof b==="boolean";if(c.isFunction(a))return this.each(function(e){var j=c(this);j.toggleClass(a.call(this,e,j.attr("class"),b),b)});return this.each(function(){if(d==="string")for(var e,j=0,i=c(this),o=b,k=a.split(ca);e=k[j++];){o=f?o:!i.hasClass(e);i[o?"addClass":"removeClass"](e)}else if(d==="undefined"||d==="boolean"){this.className&&c.data(this,"__className__",this.className);this.className=
+this.className||a===false?"":c.data(this,"__className__")||""}})},hasClass:function(a){a=" "+a+" ";for(var b=0,d=this.length;b<d;b++)if((" "+this[b].className+" ").replace(Aa," ").indexOf(a)>-1)return true;return false},val:function(a){if(a===w){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var j=b?d:0;for(d=b?d+1:e.length;j<d;j++){var i=
+e[j];if(i.selected){a=c(i).val();if(b)return a;f.push(a)}}return f}if(Ba.test(b.type)&&!c.support.checkOn)return b.getAttribute("value")===null?"on":b.value;return(b.value||"").replace(Za,"")}return w}var o=c.isFunction(a);return this.each(function(k){var n=c(this),r=a;if(this.nodeType===1){if(o)r=a.call(this,k,n.val());if(typeof r==="number")r+="";if(c.isArray(r)&&Ba.test(this.type))this.checked=c.inArray(n.val(),r)>=0;else if(c.nodeName(this,"select")){var u=c.makeArray(r);c("option",this).each(function(){this.selected=
+c.inArray(c(this).val(),u)>=0});if(!u.length)this.selectedIndex=-1}else this.value=r}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return w;if(f&&b in c.attrFn)return c(a)[b](d);f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==w;b=f&&c.props[b]||b;if(a.nodeType===1){var j=$a.test(b);if(b in a&&f&&!j){if(e){b==="type"&&ab.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed");
+a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:bb.test(a.nodeName)||cb.test(a.nodeName)&&a.href?0:w;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&j?a.getAttribute(b,2):a.getAttribute(b);return a===null?w:a}return c.style(a,b,d)}});var O=/\.(.*)$/,db=function(a){return a.replace(/[^\w\s\.\|`]/g,
+function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==A&&!a.frameElement)a=A;var e,j;if(d.handler){e=d;d=e.handler}if(!d.guid)d.guid=c.guid++;if(j=c.data(a)){var i=j.events=j.events||{},o=j.handle;if(!o)j.handle=o=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(o.elem,arguments):w};o.elem=a;b=b.split(" ");for(var k,n=0,r;k=b[n++];){j=e?c.extend({},e):{handler:d,data:f};if(k.indexOf(".")>-1){r=k.split(".");
+k=r.shift();j.namespace=r.slice(0).sort().join(".")}else{r=[];j.namespace=""}j.type=k;j.guid=d.guid;var u=i[k],z=c.event.special[k]||{};if(!u){u=i[k]=[];if(!z.setup||z.setup.call(a,f,r,o)===false)if(a.addEventListener)a.addEventListener(k,o,false);else a.attachEvent&&a.attachEvent("on"+k,o)}if(z.add){z.add.call(a,j);if(!j.handler.guid)j.handler.guid=d.guid}u.push(j);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){var e,j=0,i,o,k,n,r,u,z=c.data(a),
+C=z&&z.events;if(z&&C){if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(e in C)c.event.remove(a,e+b)}else{for(b=b.split(" ");e=b[j++];){n=e;i=e.indexOf(".")<0;o=[];if(!i){o=e.split(".");e=o.shift();k=new RegExp("(^|\\.)"+c.map(o.slice(0).sort(),db).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(r=C[e])if(d){n=c.event.special[e]||{};for(B=f||0;B<r.length;B++){u=r[B];if(d.guid===u.guid){if(i||k.test(u.namespace)){f==null&&r.splice(B--,1);n.remove&&n.remove.call(a,u)}if(f!=
+null)break}}if(r.length===0||f!=null&&r.length===1){if(!n.teardown||n.teardown.call(a,o)===false)Ca(a,e,z.handle);delete C[e]}}else for(var B=0;B<r.length;B++){u=r[B];if(i||k.test(u.namespace)){c.event.remove(a,n,u.handler,B);r.splice(B--,1)}}}if(c.isEmptyObject(C)){if(b=z.handle)b.elem=null;delete z.events;delete z.handle;c.isEmptyObject(z)&&c.removeData(a)}}}}},trigger:function(a,b,d,f){var e=a.type||a;if(!f){a=typeof a==="object"?a[G]?a:c.extend(c.Event(e),a):c.Event(e);if(e.indexOf("!")>=0){a.type=
+e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return w;a.result=w;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(j){}if(!a.isPropagationStopped()&&
+f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){f=a.target;var i,o=c.nodeName(f,"a")&&e==="click",k=c.event.special[e]||{};if((!k._default||k._default.call(d,a)===false)&&!o&&!(f&&f.nodeName&&c.noData[f.nodeName.toLowerCase()])){try{if(f[e]){if(i=f["on"+e])f["on"+e]=null;c.event.triggered=true;f[e]()}}catch(n){}if(i)f["on"+e]=i;c.event.triggered=false}}},handle:function(a){var b,d,f,e;a=arguments[0]=c.event.fix(a||A.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive;
+if(!b){d=a.type.split(".");a.type=d.shift();f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)")}e=c.data(this,"events");d=e[a.type];if(e&&d){d=d.slice(0);e=0;for(var j=d.length;e<j;e++){var i=d[e];if(b||f.test(i.namespace)){a.handler=i.handler;a.data=i.data;a.handleObj=i;i=i.handler.apply(this,arguments);if(i!==w){a.result=i;if(i===false){a.preventDefault();a.stopPropagation()}}if(a.isImmediatePropagationStopped())break}}}return a.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
+fix:function(a){if(a[G])return a;var b=a;a=c.Event(b);for(var d=this.props.length,f;d;){f=this.props[--d];a[f]=b[f]}if(!a.target)a.target=a.srcElement||s;if(a.target.nodeType===3)a.target=a.target.parentNode;if(!a.relatedTarget&&a.fromElement)a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement;if(a.pageX==null&&a.clientX!=null){b=s.documentElement;d=s.body;a.pageX=a.clientX+(b&&b.scrollLeft||d&&d.scrollLeft||0)-(b&&b.clientLeft||d&&d.clientLeft||0);a.pageY=a.clientY+(b&&b.scrollTop||
+d&&d.scrollTop||0)-(b&&b.clientTop||d&&d.clientTop||0)}if(!a.which&&(a.charCode||a.charCode===0?a.charCode:a.keyCode))a.which=a.charCode||a.keyCode;if(!a.metaKey&&a.ctrlKey)a.metaKey=a.ctrlKey;if(!a.which&&a.button!==w)a.which=a.button&1?1:a.button&2?3:a.button&4?2:0;return a},guid:1E8,proxy:c.proxy,special:{ready:{setup:c.bindReady,teardown:c.noop},live:{add:function(a){c.event.add(this,a.origType,c.extend({},a,{handler:oa}))},remove:function(a){var b=true,d=a.origType.replace(O,"");c.each(c.data(this,
+"events").live||[],function(){if(d===this.origType.replace(O,""))return b=false});b&&c.event.remove(this,a.origType,oa)}},beforeunload:{setup:function(a,b,d){if(this.setInterval)this.onbeforeunload=d;return false},teardown:function(a,b){if(this.onbeforeunload===b)this.onbeforeunload=null}}}};var Ca=s.removeEventListener?function(a,b,d){a.removeEventListener(b,d,false)}:function(a,b,d){a.detachEvent("on"+b,d)};c.Event=function(a){if(!this.preventDefault)return new c.Event(a);if(a&&a.type){this.originalEvent=
+a;this.type=a.type}else this.type=a;this.timeStamp=J();this[G]=true};c.Event.prototype={preventDefault:function(){this.isDefaultPrevented=Z;var a=this.originalEvent;if(a){a.preventDefault&&a.preventDefault();a.returnValue=false}},stopPropagation:function(){this.isPropagationStopped=Z;var a=this.originalEvent;if(a){a.stopPropagation&&a.stopPropagation();a.cancelBubble=true}},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=Z;this.stopPropagation()},isDefaultPrevented:Y,isPropagationStopped:Y,
+isImmediatePropagationStopped:Y};var Da=function(a){var b=a.relatedTarget;try{for(;b&&b!==this;)b=b.parentNode;if(b!==this){a.type=a.data;c.event.handle.apply(this,arguments)}}catch(d){}},Ea=function(a){a.type=a.data;c.event.handle.apply(this,arguments)};c.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){c.event.special[a]={setup:function(d){c.event.add(this,b,d&&d.selector?Ea:Da,a)},teardown:function(d){c.event.remove(this,b,d&&d.selector?Ea:Da)}}});if(!c.support.submitBubbles)c.event.special.submit=
+{setup:function(){if(this.nodeName.toLowerCase()!=="form"){c.event.add(this,"click.specialSubmit",function(a){var b=a.target,d=b.type;if((d==="submit"||d==="image")&&c(b).closest("form").length)return na("submit",this,arguments)});c.event.add(this,"keypress.specialSubmit",function(a){var b=a.target,d=b.type;if((d==="text"||d==="password")&&c(b).closest("form").length&&a.keyCode===13)return na("submit",this,arguments)})}else return false},teardown:function(){c.event.remove(this,".specialSubmit")}};
+if(!c.support.changeBubbles){var da=/textarea|input|select/i,ea,Fa=function(a){var b=a.type,d=a.value;if(b==="radio"||b==="checkbox")d=a.checked;else if(b==="select-multiple")d=a.selectedIndex>-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},fa=function(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Fa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data",
+e);if(!(f===w||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:fa,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return fa.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return fa.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a,
+"_change_data",Fa(a))}},setup:function(){if(this.type==="file")return false;for(var a in ea)c.event.add(this,a+".specialChange",ea[a]);return da.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return da.test(this.nodeName)}};ea=c.event.special.change.filters}s.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}c.event.special[b]={setup:function(){this.addEventListener(a,
+d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var j in d)this[b](j,f,d[j],e);return this}if(c.isFunction(f)){e=f;f=w}var i=b==="one"?c.proxy(e,function(k){c(this).unbind(k,i);return e.apply(this,arguments)}):e;if(d==="unload"&&b!=="one")this.one(d,f,e);else{j=0;for(var o=this.length;j<o;j++)c.event.add(this[j],d,i,f)}return this}});c.fn.extend({unbind:function(a,b){if(typeof a==="object"&&
+!a.preventDefault)for(var d in a)this.unbind(d,a[d]);else{d=0;for(var f=this.length;d<f;d++)c.event.remove(this[d],a,b)}return this},delegate:function(a,b,d,f){return this.live(b,d,f,a)},undelegate:function(a,b,d){return arguments.length===0?this.unbind("live"):this.die(b,null,d,a)},trigger:function(a,b){return this.each(function(){c.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0]){a=c.Event(a);a.preventDefault();a.stopPropagation();c.event.trigger(a,b,this[0]);return a.result}},
+toggle:function(a){for(var b=arguments,d=1;d<b.length;)c.proxy(a,b[d++]);return this.click(c.proxy(a,function(f){var e=(c.data(this,"lastToggle"+a.guid)||0)%d;c.data(this,"lastToggle"+a.guid,e+1);f.preventDefault();return b[e].apply(this,arguments)||false}))},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var Ga={focus:"focusin",blur:"focusout",mouseenter:"mouseover",mouseleave:"mouseout"};c.each(["live","die"],function(a,b){c.fn[b]=function(d,f,e,j){var i,o=0,k,n,r=j||this.selector,
+u=j?this:c(this.context);if(c.isFunction(f)){e=f;f=w}for(d=(d||"").split(" ");(i=d[o++])!=null;){j=O.exec(i);k="";if(j){k=j[0];i=i.replace(O,"")}if(i==="hover")d.push("mouseenter"+k,"mouseleave"+k);else{n=i;if(i==="focus"||i==="blur"){d.push(Ga[i]+k);i+=k}else i=(Ga[i]||i)+k;b==="live"?u.each(function(){c.event.add(this,pa(i,r),{data:f,selector:r,handler:e,origType:i,origHandler:e,preType:n})}):u.unbind(pa(i,r),e)}}return this}});c.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),
+function(a,b){c.fn[b]=function(d){return d?this.bind(b,d):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});A.attachEvent&&!A.addEventListener&&A.attachEvent("onunload",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}});(function(){function a(g){for(var h="",l,m=0;g[m];m++){l=g[m];if(l.nodeType===3||l.nodeType===4)h+=l.nodeValue;else if(l.nodeType!==8)h+=a(l.childNodes)}return h}function b(g,h,l,m,q,p){q=0;for(var v=m.length;q<v;q++){var t=m[q];
+if(t){t=t[g];for(var y=false;t;){if(t.sizcache===l){y=m[t.sizset];break}if(t.nodeType===1&&!p){t.sizcache=l;t.sizset=q}if(t.nodeName.toLowerCase()===h){y=t;break}t=t[g]}m[q]=y}}}function d(g,h,l,m,q,p){q=0;for(var v=m.length;q<v;q++){var t=m[q];if(t){t=t[g];for(var y=false;t;){if(t.sizcache===l){y=m[t.sizset];break}if(t.nodeType===1){if(!p){t.sizcache=l;t.sizset=q}if(typeof h!=="string"){if(t===h){y=true;break}}else if(k.filter(h,[t]).length>0){y=t;break}}t=t[g]}m[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
+e=0,j=Object.prototype.toString,i=false,o=true;[0,0].sort(function(){o=false;return 0});var k=function(g,h,l,m){l=l||[];var q=h=h||s;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||typeof g!=="string")return l;for(var p=[],v,t,y,S,H=true,M=x(h),I=g;(f.exec(""),v=f.exec(I))!==null;){I=v[3];p.push(v[1]);if(v[2]){S=v[3];break}}if(p.length>1&&r.exec(g))if(p.length===2&&n.relative[p[0]])t=ga(p[0]+p[1],h);else for(t=n.relative[p[0]]?[h]:k(p.shift(),h);p.length;){g=p.shift();if(n.relative[g])g+=p.shift();
+t=ga(g,t)}else{if(!m&&p.length>1&&h.nodeType===9&&!M&&n.match.ID.test(p[0])&&!n.match.ID.test(p[p.length-1])){v=k.find(p.shift(),h,M);h=v.expr?k.filter(v.expr,v.set)[0]:v.set[0]}if(h){v=m?{expr:p.pop(),set:z(m)}:k.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=v.expr?k.filter(v.expr,v.set):v.set;if(p.length>0)y=z(t);else H=false;for(;p.length;){var D=p.pop();v=D;if(n.relative[D])v=p.pop();else D="";if(v==null)v=h;n.relative[D](y,v,M)}}else y=[]}y||(y=t);y||k.error(D||
+g);if(j.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))l.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&l.push(t[g]);else l.push.apply(l,y);else z(y,l);if(S){k(S,q,l,m);k.uniqueSort(l)}return l};k.uniqueSort=function(g){if(B){i=o;g.sort(B);if(i)for(var h=1;h<g.length;h++)g[h]===g[h-1]&&g.splice(h--,1)}return g};k.matches=function(g,h){return k(g,null,null,h)};k.find=function(g,h,l){var m,q;if(!g)return[];
+for(var p=0,v=n.order.length;p<v;p++){var t=n.order[p];if(q=n.leftMatch[t].exec(g)){var y=q[1];q.splice(1,1);if(y.substr(y.length-1)!=="\\"){q[1]=(q[1]||"").replace(/\\/g,"");m=n.find[t](q,h,l);if(m!=null){g=g.replace(n.match[t],"");break}}}}m||(m=h.getElementsByTagName("*"));return{set:m,expr:g}};k.filter=function(g,h,l,m){for(var q=g,p=[],v=h,t,y,S=h&&h[0]&&x(h[0]);g&&h.length;){for(var H in n.filter)if((t=n.leftMatch[H].exec(g))!=null&&t[2]){var M=n.filter[H],I,D;D=t[1];y=false;t.splice(1,1);if(D.substr(D.length-
+1)!=="\\"){if(v===p)p=[];if(n.preFilter[H])if(t=n.preFilter[H](t,v,l,p,m,S)){if(t===true)continue}else y=I=true;if(t)for(var U=0;(D=v[U])!=null;U++)if(D){I=M(D,t,U,v);var Ha=m^!!I;if(l&&I!=null)if(Ha)y=true;else v[U]=false;else if(Ha){p.push(D);y=true}}if(I!==w){l||(v=p);g=g.replace(n.match[H],"");if(!y)return[];break}}}if(g===q)if(y==null)k.error(g);else break;q=g}return v};k.error=function(g){throw"Syntax error, unrecognized expression: "+g;};var n=k.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF-]|\\.)+)/,
+CLASS:/\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(g){return g.getAttribute("href")}},
+relative:{"+":function(g,h){var l=typeof h==="string",m=l&&!/\W/.test(h);l=l&&!m;if(m)h=h.toLowerCase();m=0;for(var q=g.length,p;m<q;m++)if(p=g[m]){for(;(p=p.previousSibling)&&p.nodeType!==1;);g[m]=l||p&&p.nodeName.toLowerCase()===h?p||false:p===h}l&&k.filter(h,g,true)},">":function(g,h){var l=typeof h==="string";if(l&&!/\W/.test(h)){h=h.toLowerCase();for(var m=0,q=g.length;m<q;m++){var p=g[m];if(p){l=p.parentNode;g[m]=l.nodeName.toLowerCase()===h?l:false}}}else{m=0;for(q=g.length;m<q;m++)if(p=g[m])g[m]=
+l?p.parentNode:p.parentNode===h;l&&k.filter(h,g,true)}},"":function(g,h,l){var m=e++,q=d;if(typeof h==="string"&&!/\W/.test(h)){var p=h=h.toLowerCase();q=b}q("parentNode",h,m,g,p,l)},"~":function(g,h,l){var m=e++,q=d;if(typeof h==="string"&&!/\W/.test(h)){var p=h=h.toLowerCase();q=b}q("previousSibling",h,m,g,p,l)}},find:{ID:function(g,h,l){if(typeof h.getElementById!=="undefined"&&!l)return(g=h.getElementById(g[1]))?[g]:[]},NAME:function(g,h){if(typeof h.getElementsByName!=="undefined"){var l=[];
+h=h.getElementsByName(g[1]);for(var m=0,q=h.length;m<q;m++)h[m].getAttribute("name")===g[1]&&l.push(h[m]);return l.length===0?null:l}},TAG:function(g,h){return h.getElementsByTagName(g[1])}},preFilter:{CLASS:function(g,h,l,m,q,p){g=" "+g[1].replace(/\\/g,"")+" ";if(p)return g;p=0;for(var v;(v=h[p])!=null;p++)if(v)if(q^(v.className&&(" "+v.className+" ").replace(/[\t\n]/g," ").indexOf(g)>=0))l||m.push(v);else if(l)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},
+CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,l,m,q,p){h=g[1].replace(/\\/g,"");if(!p&&n.attrMap[h])g[1]=n.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,l,m,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=k(g[3],null,null,h);else{g=k.filter(g[3],h,l,true^q);l||m.push.apply(m,
+g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,l){return!!k(l[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},
+text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},
+setFilters:{first:function(g,h){return h===0},last:function(g,h,l,m){return h===m.length-1},even:function(g,h){return h%2===0},odd:function(g,h){return h%2===1},lt:function(g,h,l){return h<l[3]-0},gt:function(g,h,l){return h>l[3]-0},nth:function(g,h,l){return l[3]-0===h},eq:function(g,h,l){return l[3]-0===h}},filter:{PSEUDO:function(g,h,l,m){var q=h[1],p=n.filters[q];if(p)return p(g,l,h,m);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h=
+h[3];l=0;for(m=h.length;l<m;l++)if(h[l]===g)return false;return true}else k.error("Syntax error, unrecognized expression: "+q)},CHILD:function(g,h){var l=h[1],m=g;switch(l){case "only":case "first":for(;m=m.previousSibling;)if(m.nodeType===1)return false;if(l==="first")return true;m=g;case "last":for(;m=m.nextSibling;)if(m.nodeType===1)return false;return true;case "nth":l=h[2];var q=h[3];if(l===1&&q===0)return true;h=h[0];var p=g.parentNode;if(p&&(p.sizcache!==h||!g.nodeIndex)){var v=0;for(m=p.firstChild;m;m=
+m.nextSibling)if(m.nodeType===1)m.nodeIndex=++v;p.sizcache=h}g=g.nodeIndex-q;return l===0?g===0:g%l===0&&g/l>=0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var l=h[1];g=n.attrHandle[l]?n.attrHandle[l](g):g[l]!=null?g[l]:g.getAttribute(l);l=g+"";var m=h[2];h=h[4];return g==null?m==="!=":m===
+"="?l===h:m==="*="?l.indexOf(h)>=0:m==="~="?(" "+l+" ").indexOf(h)>=0:!h?l&&g!==false:m==="!="?l!==h:m==="^="?l.indexOf(h)===0:m==="$="?l.substr(l.length-h.length)===h:m==="|="?l===h||l.substr(0,h.length+1)===h+"-":false},POS:function(g,h,l,m){var q=n.setFilters[h[2]];if(q)return q(g,l,h,m)}}},r=n.match.POS;for(var u in n.match){n.match[u]=new RegExp(n.match[u].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[u]=new RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[u].source.replace(/\\(\d+)/g,function(g,
+h){return"\\"+(h-0+1)}))}var z=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};try{Array.prototype.slice.call(s.documentElement.childNodes,0)}catch(C){z=function(g,h){h=h||[];if(j.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var l=0,m=g.length;l<m;l++)h.push(g[l]);else for(l=0;g[l];l++)h.push(g[l]);return h}}var B;if(s.documentElement.compareDocumentPosition)B=function(g,h){if(!g.compareDocumentPosition||
+!h.compareDocumentPosition){if(g==h)i=true;return g.compareDocumentPosition?-1:1}g=g.compareDocumentPosition(h)&4?-1:g===h?0:1;if(g===0)i=true;return g};else if("sourceIndex"in s.documentElement)B=function(g,h){if(!g.sourceIndex||!h.sourceIndex){if(g==h)i=true;return g.sourceIndex?-1:1}g=g.sourceIndex-h.sourceIndex;if(g===0)i=true;return g};else if(s.createRange)B=function(g,h){if(!g.ownerDocument||!h.ownerDocument){if(g==h)i=true;return g.ownerDocument?-1:1}var l=g.ownerDocument.createRange(),m=
+h.ownerDocument.createRange();l.setStart(g,0);l.setEnd(g,0);m.setStart(h,0);m.setEnd(h,0);g=l.compareBoundaryPoints(Range.START_TO_END,m);if(g===0)i=true;return g};(function(){var g=s.createElement("div"),h="script"+(new Date).getTime();g.innerHTML="<a name='"+h+"'/>";var l=s.documentElement;l.insertBefore(g,l.firstChild);if(s.getElementById(h)){n.find.ID=function(m,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(m[1]))?q.id===m[1]||typeof q.getAttributeNode!=="undefined"&&
+q.getAttributeNode("id").nodeValue===m[1]?[q]:w:[]};n.filter.ID=function(m,q){var p=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&p&&p.nodeValue===q}}l.removeChild(g);l=g=null})();(function(){var g=s.createElement("div");g.appendChild(s.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(h,l){l=l.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var m=0;l[m];m++)l[m].nodeType===1&&h.push(l[m]);l=h}return l};g.innerHTML="<a href='#'></a>";
+if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(h){return h.getAttribute("href",2)};g=null})();s.querySelectorAll&&function(){var g=k,h=s.createElement("div");h.innerHTML="<p class='TEST'></p>";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){k=function(m,q,p,v){q=q||s;if(!v&&q.nodeType===9&&!x(q))try{return z(q.querySelectorAll(m),p)}catch(t){}return g(m,q,p,v)};for(var l in g)k[l]=g[l];h=null}}();
+(function(){var g=s.createElement("div");g.innerHTML="<div class='test e'></div><div class='test'></div>";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(h,l,m){if(typeof l.getElementsByClassName!=="undefined"&&!m)return l.getElementsByClassName(h[1])};g=null}}})();var E=s.compareDocumentPosition?function(g,h){return!!(g.compareDocumentPosition(h)&16)}:
+function(g,h){return g!==h&&(g.contains?g.contains(h):true)},x=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},ga=function(g,h){var l=[],m="",q;for(h=h.nodeType?[h]:h;q=n.match.PSEUDO.exec(g);){m+=q[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;q=0;for(var p=h.length;q<p;q++)k(g,h[q],l);return k.filter(m,l)};c.find=k;c.expr=k.selectors;c.expr[":"]=c.expr.filters;c.unique=k.uniqueSort;c.text=a;c.isXMLDoc=x;c.contains=E})();var eb=/Until$/,fb=/^(?:parents|prevUntil|prevAll)/,
+gb=/,/;R=Array.prototype.slice;var Ia=function(a,b,d){if(c.isFunction(b))return c.grep(a,function(e,j){return!!b.call(e,j,e)===d});else if(b.nodeType)return c.grep(a,function(e){return e===b===d});else if(typeof b==="string"){var f=c.grep(a,function(e){return e.nodeType===1});if(Ua.test(b))return c.filter(b,f,!d);else b=c.filter(b,f)}return c.grep(a,function(e){return c.inArray(e,b)>=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f<e;f++){d=b.length;
+c.find(a,this[f],b);if(f>0)for(var j=d;j<b.length;j++)for(var i=0;i<d;i++)if(b[i]===b[j]){b.splice(j--,1);break}}return b},has:function(a){var b=c(a);return this.filter(function(){for(var d=0,f=b.length;d<f;d++)if(c.contains(this,b[d]))return true})},not:function(a){return this.pushStack(Ia(this,a,false),"not",a)},filter:function(a){return this.pushStack(Ia(this,a,true),"filter",a)},is:function(a){return!!a&&c.filter(a,this).length>0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,j=
+{},i;if(f&&a.length){e=0;for(var o=a.length;e<o;e++){i=a[e];j[i]||(j[i]=c.expr.match.POS.test(i)?c(i,b||this.context):i)}for(;f&&f.ownerDocument&&f!==b;){for(i in j){e=j[i];if(e.jquery?e.index(f)>-1:c(f).is(e)){d.push({selector:i,elem:f});delete j[i]}}f=f.parentNode}}return d}var k=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(n,r){for(;r&&r.ownerDocument&&r!==b;){if(k?k.index(r)>-1:c(r).is(a))return r;r=r.parentNode}return null})},index:function(a){if(!a||typeof a===
+"string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),a);return this.pushStack(qa(a[0])||qa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode",
+d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")?
+a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);eb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):e;if((this.length>1||gb.test(f))&&fb.test(a))e=e.reverse();return this.pushStack(e,a,R.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===w||a.nodeType!==1||!c(a).is(d));){a.nodeType===
+1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var Ja=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ka=/(<([\w:]+)[^>]*?)\/>/g,hb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,La=/<([\w:]+)/,ib=/<tbody/i,jb=/<|&#?\w+;/,ta=/<script|<object|<embed|<option|<style/i,ua=/checked\s*(?:[^=]|=\s*.checked.)/i,Ma=function(a,b,d){return hb.test(d)?
+a:b+"></"+d+">"},F={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div<div>","</div>"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d=
+c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==w)return this.empty().append((this[0]&&this[0].ownerDocument||s).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this},
+wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})},
+prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,
+this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,f;(f=this[d])!=null;d++)if(!a||c.filter(a,[f]).length){if(!b&&f.nodeType===1){c.cleanData(f.getElementsByTagName("*"));c.cleanData([f])}f.parentNode&&f.parentNode.removeChild(f)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild);
+return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Ja,"").replace(/=([^="'>\s]+\/)>/g,'="$1">').replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){ra(this,b);ra(this.find("*"),b.find("*"))}return b},html:function(a){if(a===w)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Ja,
+""):null;else if(typeof a==="string"&&!ta.test(a)&&(c.support.leadingWhitespace||!V.test(a))&&!F[(La.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Ka,Ma);try{for(var b=0,d=this.length;b<d;b++)if(this[b].nodeType===1){c.cleanData(this[b].getElementsByTagName("*"));this[b].innerHTML=a}}catch(f){this.empty().append(a)}}else c.isFunction(a)?this.each(function(e){var j=c(this),i=j.html();j.empty().append(function(){return a.call(this,e,i)})}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&
+this[0].parentNode){if(c.isFunction(a))return this.each(function(b){var d=c(this),f=d.html();d.replaceWith(a.call(this,b,f))});if(typeof a!=="string")a=c(a).detach();return this.each(function(){var b=this.nextSibling,d=this.parentNode;c(this).remove();b?c(b).before(a):c(d).append(a)})}else return this.pushStack(c(c.isFunction(a)?a():a),"replaceWith",a)},detach:function(a){return this.remove(a,true)},domManip:function(a,b,d){function f(u){return c.nodeName(u,"table")?u.getElementsByTagName("tbody")[0]||
+u.appendChild(u.ownerDocument.createElement("tbody")):u}var e,j,i=a[0],o=[],k;if(!c.support.checkClone&&arguments.length===3&&typeof i==="string"&&ua.test(i))return this.each(function(){c(this).domManip(a,b,d,true)});if(c.isFunction(i))return this.each(function(u){var z=c(this);a[0]=i.call(this,u,b?z.html():w);z.domManip(a,b,d)});if(this[0]){e=i&&i.parentNode;e=c.support.parentNode&&e&&e.nodeType===11&&e.childNodes.length===this.length?{fragment:e}:sa(a,this,o);k=e.fragment;if(j=k.childNodes.length===
+1?(k=k.firstChild):k.firstChild){b=b&&c.nodeName(j,"tr");for(var n=0,r=this.length;n<r;n++)d.call(b?f(this[n],j):this[n],n>0||e.cacheable||this.length>1?k.cloneNode(true):k)}o.length&&c.each(o,Qa)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var f=[];d=c(d);var e=this.length===1&&this[0].parentNode;if(e&&e.nodeType===11&&e.childNodes.length===1&&d.length===1){d[b](this[0]);
+return this}else{e=0;for(var j=d.length;e<j;e++){var i=(e>0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),i);f=f.concat(i)}return this.pushStack(f,a,d.selector)}}});c.extend({clean:function(a,b,d,f){b=b||s;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||s;for(var e=[],j=0,i;(i=a[j])!=null;j++){if(typeof i==="number")i+="";if(i){if(typeof i==="string"&&!jb.test(i))i=b.createTextNode(i);else if(typeof i==="string"){i=i.replace(Ka,Ma);var o=(La.exec(i)||["",
+""])[1].toLowerCase(),k=F[o]||F._default,n=k[0],r=b.createElement("div");for(r.innerHTML=k[1]+i+k[2];n--;)r=r.lastChild;if(!c.support.tbody){n=ib.test(i);o=o==="table"&&!n?r.firstChild&&r.firstChild.childNodes:k[1]==="<table>"&&!n?r.childNodes:[];for(k=o.length-1;k>=0;--k)c.nodeName(o[k],"tbody")&&!o[k].childNodes.length&&o[k].parentNode.removeChild(o[k])}!c.support.leadingWhitespace&&V.test(i)&&r.insertBefore(b.createTextNode(V.exec(i)[0]),r.firstChild);i=r.childNodes}if(i.nodeType)e.push(i);else e=
+c.merge(e,i)}}if(d)for(j=0;e[j];j++)if(f&&c.nodeName(e[j],"script")&&(!e[j].type||e[j].type.toLowerCase()==="text/javascript"))f.push(e[j].parentNode?e[j].parentNode.removeChild(e[j]):e[j]);else{e[j].nodeType===1&&e.splice.apply(e,[j+1,0].concat(c.makeArray(e[j].getElementsByTagName("script"))));d.appendChild(e[j])}return e},cleanData:function(a){for(var b,d,f=c.cache,e=c.event.special,j=c.support.deleteExpando,i=0,o;(o=a[i])!=null;i++)if(d=o[c.expando]){b=f[d];if(b.events)for(var k in b.events)e[k]?
+c.event.remove(o,k):Ca(o,k,b.handle);if(j)delete o[c.expando];else o.removeAttribute&&o.removeAttribute(c.expando);delete f[d]}}});var kb=/z-?index|font-?weight|opacity|zoom|line-?height/i,Na=/alpha\([^)]*\)/,Oa=/opacity=([^)]*)/,ha=/float/i,ia=/-([a-z])/ig,lb=/([A-Z])/g,mb=/^-?\d+(?:px)?$/i,nb=/^-?\d/,ob={position:"absolute",visibility:"hidden",display:"block"},pb=["Left","Right"],qb=["Top","Bottom"],rb=s.defaultView&&s.defaultView.getComputedStyle,Pa=c.support.cssFloat?"cssFloat":"styleFloat",ja=
+function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return X(this,a,b,true,function(d,f,e){if(e===w)return c.curCSS(d,f);if(typeof e==="number"&&!kb.test(f))e+="px";c.style(d,f,e)})};c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return w;if((b==="width"||b==="height")&&parseFloat(d)<0)d=w;var f=a.style||a,e=d!==w;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""==="NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter=
+Na.test(a)?a.replace(Na,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Oa.exec(f.filter)[1])/100+"":""}if(ha.test(b))b=Pa;b=b.replace(ia,ja);if(e)f[b]=d;return f[b]},css:function(a,b,d,f){if(b==="width"||b==="height"){var e,j=b==="width"?pb:qb;function i(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(j,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+=parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a,
+"border"+this+"Width",true))||0})}a.offsetWidth!==0?i():c.swap(a,ob,i);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&a.currentStyle){f=Oa.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ha.test(b))b=Pa;if(!d&&e&&e[b])f=e[b];else if(rb){if(ha.test(b))b="float";b=b.replace(lb,"-$1").toLowerCase();e=a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f=
+a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ia,ja);f=a.currentStyle[b]||a.currentStyle[d];if(!mb.test(f)&&nb.test(f)){b=e.left;var j=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=j}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]=f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b=
+a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var sb=J(),tb=/<script(.|\s)*?\/script>/gi,ub=/select|textarea/i,vb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,N=/=\?(&|$)/,ka=/\?/,wb=/(\?|&)_=.*?(&|$)/,xb=/^(\w+:)?\/\/([^\/?#]+)/,yb=/%20/g,zb=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!==
+"string")return zb.call(this,a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);f="POST"}var j=this;c.ajax({url:a,type:f,dataType:"html",data:b,complete:function(i,o){if(o==="success"||o==="notmodified")j.html(e?c("<div />").append(i.responseText.replace(tb,"")).find(e):i.responseText);d&&j.each(d,[i.responseText,o,i])}});return this},
+serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ub.test(this.nodeName)||vb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),
+function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href,
+global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:A.XMLHttpRequest&&(A.location.protocol!=="file:"||!A.ActiveXObject)?function(){return new A.XMLHttpRequest}:function(){try{return new A.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&&
+e.success.call(k,o,i,x);e.global&&f("ajaxSuccess",[x,e])}function d(){e.complete&&e.complete.call(k,x,i);e.global&&f("ajaxComplete",[x,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}function f(q,p){(e.context?c(e.context):c.event).trigger(q,p)}var e=c.extend(true,{},c.ajaxSettings,a),j,i,o,k=a&&a.context||e,n=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data,e.traditional);if(e.dataType==="jsonp"){if(n==="GET")N.test(e.url)||(e.url+=(ka.test(e.url)?
+"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!N.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&N.test(e.data)||N.test(e.url))){j=e.jsonpCallback||"jsonp"+sb++;if(e.data)e.data=(e.data+"").replace(N,"="+j+"$1");e.url=e.url.replace(N,"="+j+"$1");e.dataType="script";A[j]=A[j]||function(q){o=q;b();d();A[j]=w;try{delete A[j]}catch(p){}z&&z.removeChild(C)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache===
+false&&n==="GET"){var r=J(),u=e.url.replace(wb,"$1_="+r+"$2");e.url=u+(u===e.url?(ka.test(e.url)?"&":"?")+"_="+r:"")}if(e.data&&n==="GET")e.url+=(ka.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&c.event.trigger("ajaxStart");r=(r=xb.exec(e.url))&&(r[1]&&r[1]!==location.protocol||r[2]!==location.host);if(e.dataType==="script"&&n==="GET"&&r){var z=s.getElementsByTagName("head")[0]||s.documentElement,C=s.createElement("script");C.src=e.url;if(e.scriptCharset)C.charset=e.scriptCharset;if(!j){var B=
+false;C.onload=C.onreadystatechange=function(){if(!B&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){B=true;b();d();C.onload=C.onreadystatechange=null;z&&C.parentNode&&z.removeChild(C)}}}z.insertBefore(C,z.firstChild);return w}var E=false,x=e.xhr();if(x){e.username?x.open(n,e.url,e.async,e.username,e.password):x.open(n,e.url,e.async);try{if(e.data||a&&a.contentType)x.setRequestHeader("Content-Type",e.contentType);if(e.ifModified){c.lastModified[e.url]&&x.setRequestHeader("If-Modified-Since",
+c.lastModified[e.url]);c.etag[e.url]&&x.setRequestHeader("If-None-Match",c.etag[e.url])}r||x.setRequestHeader("X-Requested-With","XMLHttpRequest");x.setRequestHeader("Accept",e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(ga){}if(e.beforeSend&&e.beforeSend.call(k,x,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");x.abort();return false}e.global&&f("ajaxSend",[x,e]);var g=x.onreadystatechange=function(q){if(!x||x.readyState===0||q==="abort"){E||
+d();E=true;if(x)x.onreadystatechange=c.noop}else if(!E&&x&&(x.readyState===4||q==="timeout")){E=true;x.onreadystatechange=c.noop;i=q==="timeout"?"timeout":!c.httpSuccess(x)?"error":e.ifModified&&c.httpNotModified(x,e.url)?"notmodified":"success";var p;if(i==="success")try{o=c.httpData(x,e.dataType,e)}catch(v){i="parsererror";p=v}if(i==="success"||i==="notmodified")j||b();else c.handleError(e,x,i,p);d();q==="timeout"&&x.abort();if(e.async)x=null}};try{var h=x.abort;x.abort=function(){x&&h.call(x);
+g("abort")}}catch(l){}e.async&&e.timeout>0&&setTimeout(function(){x&&!E&&g("timeout")},e.timeout);try{x.send(n==="POST"||n==="PUT"||n==="DELETE"?e.data:null)}catch(m){c.handleError(e,x,null,m);d()}e.async||g();return x}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||a,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status===
+1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;e&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b===
+"json"||!b&&f.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(i,o){if(c.isArray(o))c.each(o,function(k,n){b||/\[\]$/.test(i)?f(i,n):d(i+"["+(typeof n==="object"||c.isArray(n)?k:"")+"]",n)});else!b&&o!=null&&typeof o==="object"?c.each(o,function(k,n){d(i+"["+k+"]",n)}):f(i,o)}function f(i,o){o=c.isFunction(o)?o():o;e[e.length]=encodeURIComponent(i)+"="+encodeURIComponent(o)}var e=[];if(b===w)b=c.ajaxSettings.traditional;
+if(c.isArray(a)||a.jquery)c.each(a,function(){f(this.name,this.value)});else for(var j in a)d(j,a[j]);return e.join("&").replace(yb,"+")}});var la={},Ab=/toggle|show|hide/,Bb=/^([+-]=)?([\d+-.]+)(.*)$/,W,va=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a||a===0)return this.animate(K("show",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");
+this[a].style.display=d||"";if(c.css(this[a],"display")==="none"){d=this[a].nodeName;var f;if(la[d])f=la[d];else{var e=c("<"+d+" />").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();la[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a<b;a++)this[a].style.display=c.data(this[a],"olddisplay")||"";return this}},hide:function(a,b){if(a||a===0)return this.animate(K("hide",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");!d&&d!=="none"&&c.data(this[a],
+"olddisplay",c.css(this[a],"display"))}a=0;for(b=this.length;a<b;a++)this[a].style.display="none";return this}},_toggle:c.fn.toggle,toggle:function(a,b){var d=typeof a==="boolean";if(c.isFunction(a)&&c.isFunction(b))this._toggle.apply(this,arguments);else a==null||d?this.each(function(){var f=d?a:c(this).is(":hidden");c(this)[f?"show":"hide"]()}):this.animate(K("toggle",3),a,b);return this},fadeTo:function(a,b,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,d)},
+animate:function(a,b,d,f){var e=c.speed(b,d,f);if(c.isEmptyObject(a))return this.each(e.complete);return this[e.queue===false?"each":"queue"](function(){var j=c.extend({},e),i,o=this.nodeType===1&&c(this).is(":hidden"),k=this;for(i in a){var n=i.replace(ia,ja);if(i!==n){a[n]=a[i];delete a[i];i=n}if(a[i]==="hide"&&o||a[i]==="show"&&!o)return j.complete.call(this);if((i==="height"||i==="width")&&this.style){j.display=c.css(this,"display");j.overflow=this.style.overflow}if(c.isArray(a[i])){(j.specialEasing=
+j.specialEasing||{})[i]=a[i][1];a[i]=a[i][0]}}if(j.overflow!=null)this.style.overflow="hidden";j.curAnim=c.extend({},a);c.each(a,function(r,u){var z=new c.fx(k,j,r);if(Ab.test(u))z[u==="toggle"?o?"show":"hide":u](a);else{var C=Bb.exec(u),B=z.cur(true)||0;if(C){u=parseFloat(C[2]);var E=C[3]||"px";if(E!=="px"){k.style[r]=(u||1)+E;B=(u||1)/z.cur(true)*B;k.style[r]=B+E}if(C[1])u=(C[1]==="-="?-1:1)*u+B;z.custom(B,u,E)}else z.custom(B,u,"")}});return true})},stop:function(a,b){var d=c.timers;a&&this.queue([]);
+this.each(function(){for(var f=d.length-1;f>=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:K("show",1),slideUp:K("hide",1),slideToggle:K("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration===
+"number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]||
+c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(j){return e.step(j)}this.startTime=J();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start;
+this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!W)W=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=J(),d=true;if(a||b>=this.options.duration+this.startTime){this.now=
+this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem,
+e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b<a.length;b++)a[b]()||a.splice(b--,1);a.length||
+c.fx.stop()},stop:function(){clearInterval(W);W=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){c.style(a.elem,"opacity",a.now)},_default:function(a){if(a.elem.style&&a.elem.style[a.prop]!=null)a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit;else a.elem[a.prop]=a.now}}});if(c.expr&&c.expr.filters)c.expr.filters.animated=function(a){return c.grep(c.timers,function(b){return a===b.elem}).length};c.fn.offset="getBoundingClientRect"in s.documentElement?
+function(a){var b=this[0];if(a)return this.each(function(e){c.offset.setOffset(this,a,e)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);var d=b.getBoundingClientRect(),f=b.ownerDocument;b=f.body;f=f.documentElement;return{top:d.top+(self.pageYOffset||c.support.boxModel&&f.scrollTop||b.scrollTop)-(f.clientTop||b.clientTop||0),left:d.left+(self.pageXOffset||c.support.boxModel&&f.scrollLeft||b.scrollLeft)-(f.clientLeft||b.clientLeft||0)}}:function(a){var b=
+this[0];if(a)return this.each(function(r){c.offset.setOffset(this,a,r)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);c.offset.initialize();var d=b.offsetParent,f=b,e=b.ownerDocument,j,i=e.documentElement,o=e.body;f=(e=e.defaultView)?e.getComputedStyle(b,null):b.currentStyle;for(var k=b.offsetTop,n=b.offsetLeft;(b=b.parentNode)&&b!==o&&b!==i;){if(c.offset.supportsFixedPosition&&f.position==="fixed")break;j=e?e.getComputedStyle(b,null):b.currentStyle;
+k-=b.scrollTop;n-=b.scrollLeft;if(b===d){k+=b.offsetTop;n+=b.offsetLeft;if(c.offset.doesNotAddBorder&&!(c.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(b.nodeName))){k+=parseFloat(j.borderTopWidth)||0;n+=parseFloat(j.borderLeftWidth)||0}f=d;d=b.offsetParent}if(c.offset.subtractsBorderForOverflowNotVisible&&j.overflow!=="visible"){k+=parseFloat(j.borderTopWidth)||0;n+=parseFloat(j.borderLeftWidth)||0}f=j}if(f.position==="relative"||f.position==="static"){k+=o.offsetTop;n+=o.offsetLeft}if(c.offset.supportsFixedPosition&&
+f.position==="fixed"){k+=Math.max(i.scrollTop,o.scrollTop);n+=Math.max(i.scrollLeft,o.scrollLeft)}return{top:k,left:n}};c.offset={initialize:function(){var a=s.body,b=s.createElement("div"),d,f,e,j=parseFloat(c.curCSS(a,"marginTop",true))||0;c.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"});b.innerHTML="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";
+a.insertBefore(b,a.firstChild);d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==j;a.removeChild(b);
+c.offset.initialize=c.noop},bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),j=parseInt(c.curCSS(a,"top",true),10)||0,i=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a,
+d,e);d={top:b.top-e.top+j,left:b.left-e.left+i};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top-
+f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||s.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],j;if(!e)return null;if(f!==w)return this.each(function(){if(j=wa(this))j.scrollTo(!a?f:c(j).scrollLeft(),a?f:c(j).scrollTop());else this[d]=f});else return(j=wa(e))?"pageXOffset"in j?j[a?"pageYOffset":
+"pageXOffset"]:c.support.boxModel&&j.document.documentElement[d]||j.document.body[d]:e[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;if(c.isFunction(f))return this.each(function(j){var i=c(this);i[d](f.call(this,j,i[d]()))});return"scrollTo"in
+e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===w?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});A.jQuery=A.$=c})(window);
diff --git a/lib/common_test/priv/jquery.tablesorter.min.js b/lib/common_test/priv/jquery.tablesorter.min.js new file mode 100644 index 0000000000..b8605df1e7 --- /dev/null +++ b/lib/common_test/priv/jquery.tablesorter.min.js @@ -0,0 +1,4 @@ + +(function($){$.extend({tablesorter:new +function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",cssChildRow:"expand-child",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,sortLocaleCompare:true,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'/\.|\,/g',onRenderHeader:null,selectorHeaders:'thead th',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}if(table.tBodies.length==0)return;var rows=table.tBodies[0].rows;if(rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i<l;i++){var p=false;if($.metadata&&($($headers[i]).metadata()&&$($headers[i]).metadata().sorter)){p=getParserById($($headers[i]).metadata().sorter);}else if((table.config.headers[i]&&table.config.headers[i].sorter)){p=getParserById(table.config.headers[i].sorter);}if(!p){p=detectParserForColumn(table,rows,-1,i);}if(table.config.debug){parsersDebug+="column:"+i+" parser:"+p.id+"\n";}list.push(p);}}if(table.config.debug){log(parsersDebug);}return list;};function detectParserForColumn(table,rows,rowIndex,cellIndex){var l=parsers.length,node=false,nodeValue=false,keepLooking=true;while(nodeValue==''&&keepLooking){rowIndex++;if(rows[rowIndex]){node=getNodeFromRowAndCellIndex(rows,rowIndex,cellIndex);nodeValue=trimAndGetNodeText(table.config,node);if(table.config.debug){log('Checking if value was empty on row:'+rowIndex);}}else{keepLooking=false;}}for(var i=1;i<l;i++){if(parsers[i].is(nodeValue,table,node)){return parsers[i];}}return parsers[0];}function getNodeFromRowAndCellIndex(rows,rowIndex,cellIndex){return rows[rowIndex].cells[cellIndex];}function trimAndGetNodeText(config,node){return $.trim(getElementText(config,node));}function getParserById(name){var l=parsers.length;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==name.toLowerCase()){return parsers[i];}}return false;}function buildCache(table){if(table.config.debug){var cacheTime=new Date();}var totalRows=(table.tBodies[0]&&table.tBodies[0].rows.length)||0,totalCells=(table.tBodies[0].rows[0]&&table.tBodies[0].rows[0].cells.length)||0,parsers=table.config.parsers,cache={row:[],normalized:[]};for(var i=0;i<totalRows;++i){var c=$(table.tBodies[0].rows[i]),cols=[];if(c.hasClass(table.config.cssChildRow)){cache.row[cache.row.length-1]=cache.row[cache.row.length-1].add(c);continue;}cache.row.push(c);for(var j=0;j<totalCells;++j){cols.push(parsers[j].format(getElementText(table.config,c[0].cells[j]),table,c[0].cells[j]));}cols.push(cache.normalized.length);cache.normalized.push(cols);cols=null;};if(table.config.debug){benchmark("Building cache for "+totalRows+" rows:",cacheTime);}return cache;};function getElementText(config,node){var text="";if(!node)return"";if(!config.supportsTextContent)config.supportsTextContent=node.textContent||false;if(config.textExtraction=="simple"){if(config.supportsTextContent){text=node.textContent;}else{if(node.childNodes[0]&&node.childNodes[0].hasChildNodes()){text=node.childNodes[0].innerHTML;}else{text=node.innerHTML;}}}else{if(typeof(config.textExtraction)=="function"){text=config.textExtraction(node);}else{text=$(node).text();}}return text;}function appendToTable(table,cache){if(table.config.debug){var appendTime=new Date()}var c=cache,r=c.row,n=c.normalized,totalRows=n.length,checkCell=(n[0].length-1),tableBody=$(table.tBodies[0]),rows=[];for(var i=0;i<totalRows;i++){var pos=n[i][checkCell];rows.push(r[pos]);if(!table.config.appender){var l=r[pos].length;for(var j=0;j<l;j++){tableBody[0].appendChild(r[pos][j]);}}}if(table.config.appender){table.config.appender(table,rows);}rows=null;if(table.config.debug){benchmark("Rebuilt table:",appendTime);}applyWidget(table);setTimeout(function(){$(table).trigger("sortEnd");},0);};function buildHeaders(table){if(table.config.debug){var time=new Date();}var meta=($.metadata)?true:false;var header_index=computeTableHeaderCellIndexes(table);$tableHeaders=$(table.config.selectorHeaders,table).each(function(index){this.column=header_index[this.parentNode.rowIndex+"-"+this.cellIndex];this.order=formatSortingOrder(table.config.sortInitialOrder);this.count=this.order;if(checkHeaderMetadata(this)||checkHeaderOptions(table,index))this.sortDisabled=true;if(checkHeaderOptionsSortingLocked(table,index))this.order=this.lockedOrder=checkHeaderOptionsSortingLocked(table,index);if(!this.sortDisabled){var $th=$(this).addClass(table.config.cssHeader);if(table.config.onRenderHeader)table.config.onRenderHeader.apply($th);}table.config.headerList[index]=this;});if(table.config.debug){benchmark("Built headers:",time);log($tableHeaders);}return $tableHeaders;};function computeTableHeaderCellIndexes(t){var matrix=[];var lookup={};var thead=t.getElementsByTagName('THEAD')[0];var trs=thead.getElementsByTagName('TR');for(var i=0;i<trs.length;i++){var cells=trs[i].cells;for(var j=0;j<cells.length;j++){var c=cells[j];var rowIndex=c.parentNode.rowIndex;var cellId=rowIndex+"-"+c.cellIndex;var rowSpan=c.rowSpan||1;var colSpan=c.colSpan||1 +var firstAvailCol;if(typeof(matrix[rowIndex])=="undefined"){matrix[rowIndex]=[];}for(var k=0;k<matrix[rowIndex].length+1;k++){if(typeof(matrix[rowIndex][k])=="undefined"){firstAvailCol=k;break;}}lookup[cellId]=firstAvailCol;for(var k=rowIndex;k<rowIndex+rowSpan;k++){if(typeof(matrix[k])=="undefined"){matrix[k]=[];}var matrixrow=matrix[k];for(var l=firstAvailCol;l<firstAvailCol+colSpan;l++){matrixrow[l]="x";}}}}return lookup;}function checkCellColSpan(table,rows,row){var arr=[],r=table.tHead.rows,c=r[row].cells;for(var i=0;i<c.length;i++){var cell=c[i];if(cell.colSpan>1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function checkHeaderOptionsSortingLocked(table,i){if((table.config.headers[i])&&(table.config.headers[i].lockedOrder))return table.config.headers[i].lockedOrder;return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i<l;i++){getWidgetById(c[i]).format(table);}}function getWidgetById(name){var l=widgets.length;for(var i=0;i<l;i++){if(widgets[i].id.toLowerCase()==name.toLowerCase()){return widgets[i];}}};function formatSortingOrder(v){if(typeof(v)!="Number"){return(v.toLowerCase()=="desc")?1:0;}else{return(v==1)?1:0;}}function isValueInArray(v,a){var l=a.length;for(var i=0;i<l;i++){if(a[i][0]==v){return true;}}return false;}function setHeadersCss(table,$headers,list,css){$headers.removeClass(css[0]).removeClass(css[1]);var h=[];$headers.each(function(offset){if(!this.sortDisabled){h[this.column]=$(this);}});var l=list.length;for(var i=0;i<l;i++){h[list[i][0]].addClass(css[list[i][1]]);}}function fixColumnWidth(table,$headers){var c=table.config;if(c.widthFixed){var colgroup=$('<colgroup>');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('<col>').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;i<l;i++){var s=sortList[i],o=c.headerList[s[0]];o.count=s[1];o.count++;}}function multisort(table,sortList,cache){if(table.config.debug){var sortTime=new Date();}var dynamicExp="var sortWrapper = function(a,b) {",l=sortList.length;for(var i=0;i<l;i++){var c=sortList[i][0];var order=sortList[i][1];var s=(table.config.parsers[c].type=="text")?((order==0)?makeSortFunction("text","asc",c):makeSortFunction("text","desc",c)):((order==0)?makeSortFunction("numeric","asc",c):makeSortFunction("numeric","desc",c));var e="e"+i;dynamicExp+="var "+e+" = "+s;dynamicExp+="if("+e+") { return "+e+"; } ";dynamicExp+="else { ";}var orgOrderCol=cache.normalized[0].length-1;dynamicExp+="return a["+orgOrderCol+"]-b["+orgOrderCol+"];";for(var i=0;i<l;i++){dynamicExp+="}; ";}dynamicExp+="return 0; ";dynamicExp+="}; ";if(table.config.debug){benchmark("Evaling expression:"+dynamicExp,new Date());}eval(dynamicExp);cache.normalized.sort(sortWrapper);if(table.config.debug){benchmark("Sorting on "+sortList.toString()+" and dir "+order+" time:",sortTime);}return cache;};function makeSortFunction(type,direction,index){var a="a["+index+"]",b="b["+index+"]";if(type=='text'&&direction=='asc'){return"("+a+" == "+b+" ? 0 : ("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : ("+a+" < "+b+") ? -1 : 1 )));";}else if(type=='text'&&direction=='desc'){return"("+a+" == "+b+" ? 0 : ("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : ("+b+" < "+a+") ? -1 : 1 )));";}else if(type=='numeric'&&direction=='asc'){return"("+a+" === null && "+b+" === null) ? 0 :("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : "+a+" - "+b+"));";}else if(type=='numeric'&&direction=='desc'){return"("+a+" === null && "+b+" === null) ? 0 :("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : "+b+" - "+a+"));";}};function makeSortText(i){return"((a["+i+"] < b["+i+"]) ? -1 : ((a["+i+"] > b["+i+"]) ? 1 : 0));";};function makeSortTextDesc(i){return"((b["+i+"] < a["+i+"]) ? -1 : ((b["+i+"] > a["+i+"]) ? 1 : 0));";};function makeSortNumeric(i){return"a["+i+"]-b["+i+"];";};function makeSortNumericDesc(i){return"b["+i+"]-a["+i+"];";};function sortText(a,b){if(table.config.sortLocaleCompare)return a.localeCompare(b);return((a<b)?-1:((a>b)?1:0));};function sortTextDesc(a,b){if(table.config.sortLocaleCompare)return b.localeCompare(a);return((b<a)?-1:((b>a)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$.data(this,"tablesorter",config);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){$this.trigger("sortStart");var $cell=$(this);var i=this.column;this.order=this.count++%2;if(this.lockedOrder)this.order=this.lockedOrder;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j<a.length;j++){if(a[j][0]!=i){config.sortList.push(a[j]);}}}config.sortList.push([i,this.order]);}else{if(isValueInArray(i,config.sortList)){for(var j=0;j<config.sortList.length;j++){var s=config.sortList[j],o=config.headerList[s[0]];if(s[0]==i){o.count=s[1];o.count++;s[1]=o.count%2;}}}else{config.sortList.push([i,this.order]);}};setTimeout(function(){setHeadersCss($this[0],$headers,config.sortList,sortCSS);appendToTable($this[0],multisort($this[0],config.sortList,cache));},1);return false;}}).mousedown(function(){if(config.cancelSelection){this.onselectstart=function(){return false};return false;}});$this.bind("update",function(){var me=this;setTimeout(function(){me.config.parsers=buildParserCache(me,$headers);cache=buildCache(me);},1);}).bind("updateCell",function(e,cell){var config=this.config;var pos=[(cell.parentNode.rowIndex-1),cell.cellIndex];cache.normalized[pos[0]][pos[1]]=config.parsers[pos[1]].format(getElementText(config,cell),cell);}).bind("sorton",function(e,list){$(this).trigger("sortStart");config.sortList=list;var sortList=config.sortList;updateHeaderSortCount(this,sortList);setHeadersCss(this,$headers,sortList,sortCSS);appendToTable(this,multisort(this,sortList,cache));}).bind("appendCache",function(){appendToTable(this,cache);}).bind("applyWidgetId",function(e,id){getWidgetById(id).format(this);}).bind("applyWidgets",function(){applyWidget(this);});if($.metadata&&($(this).metadata()&&$(this).metadata().sortlist)){config.sortList=$(this).metadata().sortlist;}if(config.sortList.length>0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==parser.id.toLowerCase()){a=false;}}if(a){parsers.push(parser);};};this.addWidget=function(widget){widgets.push(widget);};this.formatFloat=function(s){var i=parseFloat(s);return(isNaN(i))?0:i;};this.formatInt=function(s){var i=parseInt(s);return(isNaN(i))?0:i;};this.isDigit=function(s,config){return/^[-+]?\d*$/.test($.trim(s.replace(/[,.']/g,'')));};this.clearTableBody=function(table){if($.browser.msie){function empty(){while(this.firstChild)this.removeChild(this.firstChild);}empty.apply(table.tBodies[0]);}else{table.tBodies[0].innerHTML="";}};}});$.fn.extend({tablesorter:$.tablesorter.construct});var ts=$.tablesorter;ts.addParser({id:"text",is:function(s){return true;},format:function(s){return $.trim(s.toLocaleLowerCase());},type:"text"});ts.addParser({id:"digit",is:function(s,table){var c=table.config;return $.tablesorter.isDigit(s,c);},format:function(s){return $.tablesorter.formatFloat(s);},type:"numeric"});ts.addParser({id:"currency",is:function(s){return/^[£$€?.]/.test(s);},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/[£$€]/g),""));},type:"numeric"});ts.addParser({id:"ipAddress",is:function(s){return/^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s);},format:function(s){var a=s.split("."),r="",l=a.length;for(var i=0;i<l;i++){var item=a[i];if(item.length==2){r+="0"+item;}else{r+=item;}}return $.tablesorter.formatFloat(r);},type:"numeric"});ts.addParser({id:"url",is:function(s){return/^(https?|ftp|file):\/\/$/.test(s);},format:function(s){return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//),''));},type:"text"});ts.addParser({id:"isoDate",is:function(s){return/^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s);},format:function(s){return $.tablesorter.formatFloat((s!="")?new Date(s.replace(new RegExp(/-/g),"/")).getTime():"0");},type:"numeric"});ts.addParser({id:"percent",is:function(s){return/\%$/.test($.trim(s));},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g),""));},type:"numeric"});ts.addParser({id:"usLongDate",is:function(s){return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/));},format:function(s){return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"shortDate",is:function(s){return/\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s);},format:function(s,table){var c=table.config;s=s.replace(/\-/g,"/");if(c.dateFormat=="us"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$1/$2");}else if(c.dateFormat=="uk"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$2/$1");}else if(c.dateFormat=="dd/mm/yy"||c.dateFormat=="dd-mm-yy"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/,"$1/$2/$3");}return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"time",is:function(s){return/^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s);},format:function(s){return $.tablesorter.formatFloat(new Date("2000/01/01 "+s).getTime());},type:"numeric"});ts.addParser({id:"metadata",is:function(s){return false;},format:function(s,table,cell){var c=table.config,p=(!c.parserMetadataName)?'sortValue':c.parserMetadataName;return $(cell).metadata()[p];},type:"numeric"});ts.addWidget({id:"zebra",format:function(table){if(table.config.debug){var time=new Date();}var $tr,row=-1,odd;$("tr:visible",table.tBodies[0]).each(function(i){$tr=$(this);if(!$tr.hasClass(table.config.cssChildRow))row++;odd=(row%2==0);$tr.removeClass(table.config.widgetZebra.css[odd?0:1]).addClass(table.config.widgetZebra.css[odd?1:0])});if(table.config.debug){$.tablesorter.benchmark("Applying Zebra widget",time);}}});})(jQuery);
\ No newline at end of file diff --git a/lib/common_test/src/Makefile b/lib/common_test/src/Makefile index 037a686963..f7dce195d7 100644 --- a/lib/common_test/src/Makefile +++ b/lib/common_test/src/Makefile @@ -70,14 +70,18 @@ MODULES= \ ct_hooks\ ct_hooks_lock\ cth_log_redirect\ - cth_surefire + cth_surefire \ + ct_netconfc \ + ct_conn_log_h \ + cth_conn_log TARGET_MODULES= $(MODULES:%=$(EBIN)/%) BEAM_FILES= $(MODULES:%=$(EBIN)/%.$(EMULATOR)) ERL_FILES= $(MODULES:=.erl) HRL_FILES = \ - ct_util.hrl + ct_util.hrl \ + ct_netconfc.hrl EXTERNAL_HRL_FILES = \ ../include/ct.hrl \ ../include/ct_event.hrl diff --git a/lib/common_test/src/common_test.app.src b/lib/common_test/src/common_test.app.src index ae9a51faeb..18c1dec784 100644 --- a/lib/common_test/src/common_test.app.src +++ b/lib/common_test/src/common_test.app.src @@ -33,6 +33,8 @@ ct_master_event, ct_master_logs, ct_master_status, + ct_netconfc, + ct_conn_log_h, ct_repeat, ct_rpc, ct_run, @@ -49,6 +51,7 @@ ct_config_xml, ct_slave, cth_log_redirect, + cth_conn_log, cth_surefire ]}, {registered, [ct_logs, diff --git a/lib/common_test/src/ct.erl b/lib/common_test/src/ct.erl index 22cef776cf..d5938c5db9 100644 --- a/lib/common_test/src/ct.erl +++ b/lib/common_test/src/ct.erl @@ -68,7 +68,8 @@ capture_start/0, capture_stop/0, capture_get/0, capture_get/1, fail/1, fail/2, comment/1, comment/2, make_priv_dir/0, testcases/2, userdata/2, userdata/3, - timetrap/1, get_timetrap_info/0, sleep/1]). + timetrap/1, get_timetrap_info/0, sleep/1, + notify/2, sync_notify/2]). %% New API for manipulating with config handlers -export([add_config/2, remove_config/2]). @@ -1113,3 +1114,27 @@ sleep({seconds,Ss}) -> sleep(trunc(Ss * 1000)); sleep(Time) -> test_server:adjusted_sleep(Time). + +%%%----------------------------------------------------------------- +%%% @spec notify(Name,Data) -> ok +%%% Name = atom() +%%% Data = term() +%%% +%%% @doc <p>Sends a asynchronous notification of type <c>Name</c> with +%%% <c>Data</c>to the common_test event manager. This can later be +%%% caught by any installed event manager. </p> +%%% @see //stdlib/gen_event +notify(Name,Data) -> + ct_event:notify(Name, Data). + +%%%----------------------------------------------------------------- +%%% @spec sync_notify(Name,Data) -> ok +%%% Name = atom() +%%% Data = term() +%%% +%%% @doc <p>Sends a synchronous notification of type <c>Name</c> with +%%% <c>Data</c>to the common_test event manager. This can later be +%%% caught by any installed event manager. </p> +%%% @see //stdlib/gen_event +sync_notify(Name,Data) -> + ct_event:sync_notify(Name, Data). diff --git a/lib/common_test/src/ct_conn_log_h.erl b/lib/common_test/src/ct_conn_log_h.erl new file mode 100644 index 0000000000..f3b6781971 --- /dev/null +++ b/lib/common_test/src/ct_conn_log_h.erl @@ -0,0 +1,230 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2012. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +-module(ct_conn_log_h). + +%%% +%%% A handler that can be connected to the error_logger event +%%% handler. Writes all ct connection events. See comments in +%%% cth_conn_log for more information. +%%% + +-include("ct_util.hrl"). + +-export([init/1, + handle_event/2, handle_call/2, handle_info/2, + terminate/2]). + +-record(state, {group_leader,logs=[]}). + +-define(WIDTH,80). + +%%%----------------------------------------------------------------- +%%% Callbacks +init({GL,Logs}) -> + open_files(Logs,#state{group_leader=GL}). + +open_files([{ConnMod,{LogType,Logs}}|T],State) -> + case do_open_files(Logs,[]) of + {ok,Fds} -> + open_files(T,State#state{logs=[{ConnMod,{LogType,Fds}} | + State#state.logs]}); + Error -> + Error + end; +open_files([],State) -> + {ok,State}. + + +do_open_files([{Tag,File}|Logs],Acc) -> + case file:open(File, [write]) of + {ok,Fd} -> + do_open_files(Logs,[{Tag,Fd}|Acc]); + {error,Reason} -> + {error,{could_not_open_log,File,Reason}} + end; +do_open_files([],Acc) -> + {ok,lists:reverse(Acc)}. + +handle_event({_Type, GL, _Msg}, State) when node(GL) /= node() -> + {ok, State}; +handle_event({_Type,_GL,{Pid,{ct_connection,Action,ConnName},Report}},State) -> + Info = conn_info(Pid,#conn_log{name=ConnName,action=Action}), + write_report(now(),Info,Report,State), + {ok, State}; +handle_event({_Type,_GL,{Pid,Info=#conn_log{},Report}},State) -> + write_report(now(),conn_info(Pid,Info),Report,State), + {ok, State}; +handle_event({error_report,_,{Pid,_,[{ct_connection,ConnName}|R]}},State) -> + %% Error reports from connection + write_error(now(),conn_info(Pid,#conn_log{name=ConnName}),R,State), + {ok, State}; +handle_event(_, State) -> + {ok, State}. + +handle_info(_, State) -> + {ok, State}. + +handle_call(_Query, State) -> + {ok, {error, bad_query}, State}. + +terminate(_,#state{logs=Logs}) -> + [file:close(Fd) || {_,_,Fds} <- Logs, Fd <- Fds], + ok. + + +%%%----------------------------------------------------------------- +%%% Writing reports +write_report(Time,#conn_log{module=ConnMod}=Info,Data,State) -> + {LogType,Fd} = get_log(Info,State), + io:format(Fd,"~n~s~s~s",[format_head(ConnMod,LogType,Time), + format_title(LogType,Info), + format_data(ConnMod,LogType,Data)]). + +write_error(Time,#conn_log{module=ConnMod}=Info,Report,State) -> + case get_log(Info,State) of + {html,_} -> + %% The error will anyway be written in the html log by the + %% sasl error handler, so don't write it again. + ok; + {LogType,Fd} -> + io:format(Fd,"~n~s~s~s",[format_head(ConnMod,LogType,Time," ERROR"), + format_title(LogType,Info), + format_error(LogType,Report)]) + end. + +get_log(Info,State) -> + case proplists:get_value(Info#conn_log.module,State#state.logs) of + {html,_} -> + {html,State#state.group_leader}; + {LogType,Fds} -> + {LogType,get_fd(Info,Fds)}; + undefined -> + {html,State#state.group_leader} + end. + +get_fd(#conn_log{name=undefined},Fds) -> + proplists:get_value(default,Fds); +get_fd(#conn_log{name=ConnName},Fds) -> + case proplists:get_value(ConnName,Fds) of + undefined -> + proplists:get_value(default,Fds); + Fd -> + Fd + end. + +%%%----------------------------------------------------------------- +%%% Formatting +format_head(ConnMod,LogType,Time) -> + format_head(ConnMod,LogType,Time,""). + +format_head(ConnMod,raw,Time,Text) -> + io_lib:format("~n~p, ~p~s, ",[now_to_time(Time),ConnMod,Text]); +format_head(ConnMod,_,Time,Text) -> + Head = pad_char_end(?WIDTH,pretty_head(now_to_time(Time),ConnMod,Text),$=), + io_lib:format("~n~s",[Head]). + +format_title(raw,#conn_log{client=Client}=Info) -> + io_lib:format("Client ~p ~s ~s",[Client,actionstr(Info),serverstr(Info)]); +format_title(_,Info) -> + Title = pad_char_end(?WIDTH,pretty_title(Info),$=), + io_lib:format("~n~s", [Title]). + +format_data(_,_,NoData) when NoData == ""; NoData == <<>> -> + ""; +format_data(ConnMod,LogType,Data) -> + ConnMod:format_data(LogType,Data). + +format_error(raw,Report) -> + io_lib:format("~n~p~n",[Report]); +format_error(pretty,Report) -> + [io_lib:format("~n ~p: ~p",[K,V]) || {K,V} <- Report]. + + + + +%%%----------------------------------------------------------------- +%%% Helpers +conn_info(LoggingProc, #conn_log{client=undefined} = ConnInfo) -> + conn_info(ConnInfo#conn_log{client=LoggingProc}); +conn_info(_, ConnInfo) -> + conn_info(ConnInfo). + +conn_info(#conn_log{client=Client, module=undefined} = ConnInfo) -> + case ets:lookup(ct_connections,Client) of + [#conn{address=Address,callback=Callback}] -> + ConnInfo#conn_log{address=Address,module=Callback}; + [] -> + ConnInfo + end; +conn_info(ConnInfo) -> + ConnInfo. + + +now_to_time({_,_,MicroS}=Now) -> + {calendar:now_to_local_time(Now),MicroS}. + +pretty_head({{{Y,Mo,D},{H,Mi,S}},MicroS},ConnMod,Text0) -> + Text = string:to_upper(atom_to_list(ConnMod) ++ Text0), + io_lib:format("= ~s ==== ~s-~s-~p::~s:~s:~s,~s ", + [Text,t(D),month(Mo),Y,t(H),t(Mi),t(S), + micro2milli(MicroS)]). + +pretty_title(#conn_log{client=Client}=Info) -> + io_lib:format("= Client ~p ~s Server ~s ", + [Client,actionstr(Info),serverstr(Info)]). + +actionstr(#conn_log{action=send}) -> "----->"; +actionstr(#conn_log{action=recv}) -> "<-----"; +actionstr(#conn_log{action=open}) -> "opened session to"; +actionstr(#conn_log{action=close}) -> "closed session to"; +actionstr(_) -> "<---->". + +serverstr(#conn_log{name=undefined,address=Address}) -> + io_lib:format("~p",[Address]); +serverstr(#conn_log{name=Alias,address=Address}) -> + io_lib:format("~p(~p)",[Alias,Address]). + +month(1) -> "Jan"; +month(2) -> "Feb"; +month(3) -> "Mar"; +month(4) -> "Apr"; +month(5) -> "May"; +month(6) -> "Jun"; +month(7) -> "Jul"; +month(8) -> "Aug"; +month(9) -> "Sep"; +month(10) -> "Oct"; +month(11) -> "Nov"; +month(12) -> "Dec". + +micro2milli(X) -> + pad0(3,integer_to_list(X div 1000)). + +t(X) -> + pad0(2,integer_to_list(X)). + +pad0(N,Str) -> + M = length(Str), + lists:duplicate(N-M,$0) ++ Str. + +pad_char_end(N,Str,Char) -> + case length(lists:flatten(Str)) of + M when M<N -> Str ++ lists:duplicate(N-M,Char); + _ -> Str + end. diff --git a/lib/common_test/src/ct_event.erl b/lib/common_test/src/ct_event.erl index 3e79898ad1..998be35fda 100644 --- a/lib/common_test/src/ct_event.erl +++ b/lib/common_test/src/ct_event.erl @@ -31,7 +31,7 @@ %% API -export([start_link/0, add_handler/0, add_handler/1, stop/0]). --export([notify/1, sync_notify/1]). +-export([notify/1, notify/2, sync_notify/1,sync_notify/2]). -export([is_alive/0]). %% gen_event callbacks @@ -90,6 +90,13 @@ notify(Event) -> end. %%-------------------------------------------------------------------- +%% Function: notify(Name,Data) -> ok +%% Description: Asynchronous notification to event manager. +%%-------------------------------------------------------------------- +notify(Name, Data) -> + notify(#event{ name = Name, data = Data}). + +%%-------------------------------------------------------------------- %% Function: sync_notify(Event) -> ok %% Description: Synchronous notification to event manager. %%-------------------------------------------------------------------- @@ -102,6 +109,13 @@ sync_notify(Event) -> end. %%-------------------------------------------------------------------- +%% Function: sync_notify(Name,Data) -> ok +%% Description: Synchronous notification to event manager. +%%-------------------------------------------------------------------- +sync_notify(Name,Data) -> + sync_notify(#event{ name = Name, data = Data}). + +%%-------------------------------------------------------------------- %% Function: is_alive() -> true | false %% Description: Check if Event Manager is alive. %%-------------------------------------------------------------------- diff --git a/lib/common_test/src/ct_framework.erl b/lib/common_test/src/ct_framework.erl index 11575cd0fb..19a26872ee 100644 --- a/lib/common_test/src/ct_framework.erl +++ b/lib/common_test/src/ct_framework.erl @@ -27,7 +27,7 @@ -export([init_tc/3, end_tc/3, end_tc/4, get_suite/2, get_all_cases/1]). -export([report/2, warn/1, error_notification/4]). --export([get_logopts/0, format_comment/1, get_html_wrapper/3]). +-export([get_logopts/0, format_comment/1, get_html_wrapper/4]). -export([error_in_suite/1, init_per_suite/1, end_per_suite/1, init_per_group/2, end_per_group/2]). @@ -204,7 +204,7 @@ init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) -> data={Mod,FuncSpec}}), case catch configure(MergedInfo,MergedInfo,SuiteInfo, - FuncSpec,Config) of + FuncSpec,[],Config) of {suite0_failed,Reason} -> ct_util:set_testdata({curr_tc,{Mod,{suite0_failed,{require,Reason}}}}), {skip,{require_failed_in_suite0,Reason}}; @@ -212,12 +212,14 @@ init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) -> {auto_skip,{require_failed,Reason}}; {'EXIT',Reason} -> {auto_skip,Reason}; - {ok,Config1} -> + {ok,PostInitHook,Config1} -> case get('$test_server_framework_test') of undefined -> - ct_suite_init(Suite, FuncSpec, Config1); + ct_suite_init(Suite, FuncSpec, PostInitHook, Config1); Fun -> - case Fun(init_tc, Config1) of + PostInitHookResult = do_post_init_hook(PostInitHook, + Config1), + case Fun(init_tc, [PostInitHookResult ++ Config1]) of NewConfig when is_list(NewConfig) -> {ok,NewConfig}; Else -> @@ -226,14 +228,28 @@ init_tc2(Mod,Suite,Func,SuiteInfo,MergeResult,Config) -> end end. -ct_suite_init(Suite, Func, [Config]) when is_list(Config) -> +ct_suite_init(Suite, Func, PostInitHook, Config) when is_list(Config) -> case ct_hooks:init_tc(Suite, Func, Config) of NewConfig when is_list(NewConfig) -> - {ok, [NewConfig]}; + PostInitHookResult = do_post_init_hook(PostInitHook, NewConfig), + {ok, [PostInitHookResult ++ NewConfig]}; Else -> Else end. +do_post_init_hook(PostInitHook, Config) -> + lists:flatmap(fun({Tag,Fun}) -> + case lists:keysearch(Tag,1,Config) of + {value,_} -> + []; + false -> + case Fun() of + {error,_} -> []; + Result -> [{Tag,Result}] + end + end + end, PostInitHook). + add_defaults(Mod,Func, GroupPath) -> Suite = get_suite_name(Mod, GroupPath), case (catch Suite:suite()) of @@ -453,15 +469,16 @@ timetrap_first([],Info,[]) -> timetrap_first([],Info,Found) -> ?rev(Found) ++ ?rev(Info). -configure([{require,Required}|Rest],Info,SuiteInfo,Scope,Config) -> +configure([{require,Required}|Rest], + Info,SuiteInfo,Scope,PostInitHook,Config) -> case ct:require(Required) of ok -> - configure(Rest,Info,SuiteInfo,Scope,Config); + configure(Rest,Info,SuiteInfo,Scope,PostInitHook,Config); Error = {error,Reason} -> case required_default('_UNDEF',Required,Info, SuiteInfo,Scope) of ok -> - configure(Rest,Info,SuiteInfo,Scope,Config); + configure(Rest,Info,SuiteInfo,Scope,PostInitHook,Config); _ -> case lists:keymember(Required,2,SuiteInfo) of true -> @@ -471,14 +488,15 @@ configure([{require,Required}|Rest],Info,SuiteInfo,Scope,Config) -> end end end; -configure([{require,Name,Required}|Rest],Info,SuiteInfo,Scope,Config) -> +configure([{require,Name,Required}|Rest], + Info,SuiteInfo,Scope,PostInitHook,Config) -> case ct:require(Name,Required) of ok -> - configure(Rest,Info,SuiteInfo,Scope,Config); + configure(Rest,Info,SuiteInfo,Scope,PostInitHook,Config); Error = {error,Reason} -> case required_default(Name,Required,Info,SuiteInfo,Scope) of ok -> - configure(Rest,Info,SuiteInfo,Scope,Config); + configure(Rest,Info,SuiteInfo,Scope,PostInitHook,Config); _ -> case lists:keymember(Name,2,SuiteInfo) of true -> @@ -488,17 +506,24 @@ configure([{require,Name,Required}|Rest],Info,SuiteInfo,Scope,Config) -> end end end; -configure([{timetrap,off}|Rest],Info,SuiteInfo,Scope,Config) -> - configure(Rest,Info,SuiteInfo,Scope,Config); -configure([{timetrap,Time}|Rest],Info,SuiteInfo,Scope,Config) -> - Dog = test_server:timetrap(Time), - configure(Rest,Info,SuiteInfo,Scope,[{watchdog,Dog}|Config]); -configure([{ct_hooks, Hook} | Rest], Info, SuiteInfo, Scope, Config) -> - configure(Rest, Info, SuiteInfo, Scope, [{ct_hooks, Hook} | Config]); -configure([_|Rest],Info,SuiteInfo,Scope,Config) -> - configure(Rest,Info,SuiteInfo,Scope,Config); -configure([],_,_,_,Config) -> - {ok,[Config]}. +configure([{timetrap,off}|Rest],Info,SuiteInfo,Scope,PostInitHook,Config) -> + configure(Rest,Info,SuiteInfo,Scope,PostInitHook,Config); +configure([{timetrap,Time}|Rest],Info,SuiteInfo,Scope,PostInitHook,Config) -> + PostInitHook1 = + [{watchdog,fun() -> case test_server:get_timetrap_info() of + undefined -> + test_server:timetrap(Time); + _ -> + {error,already_set} + end + end} | PostInitHook], + configure(Rest,Info,SuiteInfo,Scope,PostInitHook1,Config); +configure([{ct_hooks,Hook}|Rest],Info,SuiteInfo,Scope,PostInitHook,Config) -> + configure(Rest,Info,SuiteInfo,Scope,PostInitHook,[{ct_hooks,Hook}|Config]); +configure([_|Rest],Info,SuiteInfo,Scope,PostInitHook,Config) -> + configure(Rest,Info,SuiteInfo,Scope,PostInitHook,Config); +configure([],_,_,_,PostInitHook,Config) -> + {ok,PostInitHook,Config}. %% the require element in Info may come from suite/0 and %% should be scoped 'suite', or come from the group info @@ -562,10 +587,8 @@ end_tc(Mod,Func,TCPid,Result,Args,Return) -> %% in case Mod == ct_framework, lookup the suite name Suite = get_suite_name(Mod, Args), - case lists:keysearch(watchdog,1,Args) of - {value,{watchdog,Dog}} -> test_server:timetrap_cancel(Dog); - false -> ok - end, + test_server:timetrap_cancel(), + %% save the testcase process pid so that it can be used %% to look up the attached trace window later case ct_util:get_testdata(interpret) of @@ -1634,5 +1657,5 @@ format_comment(Comment) -> %%%----------------------------------------------------------------- %%% @spec get_html_wrapper(TestName, PrintLabel, Cwd) -> Header -get_html_wrapper(TestName, PrintLabel, Cwd) -> - ct_logs:get_ts_html_wrapper(TestName, PrintLabel, Cwd). +get_html_wrapper(TestName, PrintLabel, Cwd, TableCols) -> + ct_logs:get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols). diff --git a/lib/common_test/src/ct_gen_conn.erl b/lib/common_test/src/ct_gen_conn.erl index 5aab4dd2dd..5df9127725 100644 --- a/lib/common_test/src/ct_gen_conn.erl +++ b/lib/common_test/src/ct_gen_conn.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2003-2010. All Rights Reserved. +%% Copyright Ericsson AB 2003-2012. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in @@ -27,7 +27,7 @@ -compile(export_all). -export([start/4, stop/1]). --export([call/2, do_within_time/2]). +-export([call/2, call/3, return/2, do_within_time/2]). -ifdef(debug). -define(dbg,true). @@ -39,17 +39,24 @@ name, address, init_data, + reconnect = true, + forward = false, + use_existing = true, + old = false, conn_pid, cb_state, ct_util_server}). %%%----------------------------------------------------------------- -%%% @spec start(Name,Address,InitData,CallbackMod) -> +%%% @spec start(Address,InitData,CallbackMod,Opts) -> %%% {ok,Handle} | {error,Reason} %%% Name = term() %%% CallbackMod = atom() %%% InitData = term() %%% Address = term() +%%% Opts = [Opt] +%%% Opt = {name,Name} | {use_existing_connection,boolean()} | +%%% {reconnect,boolean()} | {forward_messages,boolean()} %%% %%% @doc Open a connection and start the generic connection owner process. %%% @@ -60,42 +67,59 @@ %%% <code>InitData</code> and returna %%% <code>{ok,ConnectionPid,State}</code> or %%% <code>{error,Reason}</code>.</p> +%%% +%%% If no name is given, the <code>Name</code> argument in init/3 will +%%% have the value <code>undefined</code>. +%%% +%%% The callback modules must also export +%%% ``` +%%% handle_msg(Msg,From,State) -> {reply,Reply,State} | +%%% {noreply,State} | +%%% {stop,Reply,State} +%%% terminate(ConnectionPid,State) -> term() +%%% close(Handle) -> term() +%%% ''' +%%% +%%% The <code>close/1</code> callback function is actually a callback +%%% for ct_util, for closing registered connections when +%%% ct_util_server is terminated. <code>Handle</code> is the Pid of +%%% the ct_gen_conn process. +%%% +%%% If option <code>reconnect</code> is <code>true</code>, then the +%%% callback must also export +%%% ``` +%%% reconnect(Address,State) -> {ok,ConnectionPid,State} +%%% ''' +%%% +%%% If option <code>forward_messages</code> is <ocde>true</code>, then +%%% the callback must also export +%%% ``` +%%% handle_msg(Msg,State) -> {noreply,State} | {stop,State} +%%% ''' +%%% +%%% An old interface still exists. This is used by ct_telnet, ct_ftp +%%% and ct_ssh. The start function then has an explicit +%%% <code>Name</code> argument, and no <code>Opts</code> argument. The +%%% callback must export: +%%% +%%% ``` +%%% init(Name,Address,InitData) -> {ok,ConnectionPid,State} +%%% handle_msg(Msg,State) -> {Reply,State} +%%% reconnect(Address,State) -> {ok,ConnectionPid,State} +%%% terminate(ConnectionPid,State) -> term() +%%% close(Handle) -> term() +%%% ''' +%%% +start(Address,InitData,CallbackMod,Opts) when is_list(Opts) -> + do_start(Address,InitData,CallbackMod,Opts); start(Name,Address,InitData,CallbackMod) -> - case ct_util:does_connection_exist(Name,Address,CallbackMod) of - {ok,Pid} -> - log("ct_gen_conn:start","Using existing connection!\n",[]), - {ok,Pid}; - false -> - Self = self(), - Pid = spawn(fun() -> - init_gen(Self, #gen_opts{callback=CallbackMod, - name=Name, - address=Address, - init_data=InitData}) - end), - MRef = erlang:monitor(process,Pid), - receive - {connected,Pid} -> - erlang:demonitor(MRef, [flush]), - ct_util:register_connection(Name,Address,CallbackMod,Pid), - {ok,Pid}; - {Error,Pid} -> - receive {'DOWN',MRef,process,_,_} -> ok end, - Error; - {'DOWN',MRef,process,_,Reason} -> - log("ct_gen_conn:start", - "Connection process died: ~p\n", - [Reason]), - {error,{connection_process_died,Reason}} - end - end. - + do_start(Address,InitData,CallbackMod,[{name,Name},{old,true}]). %%%----------------------------------------------------------------- %%% @spec stop(Handle) -> ok %%% Handle = handle() %%% -%%% @doc Close the telnet connection and stop the process managing it. +%%% @doc Close the connection and stop the process managing it. stop(Pid) -> call(Pid,stop). @@ -103,7 +127,7 @@ stop(Pid) -> %%% @spec log(Heading,Format,Args) -> ok %%% %%% @doc Log activities on the current connection (tool-internal use only). -%%% @see ct_logs:log/3 +%%% @see ct_logs:log/3 log(Heading,Format,Args) -> log(log,[Heading,Format,Args]). @@ -111,7 +135,7 @@ log(Heading,Format,Args) -> %%% @spec start_log(Heading) -> ok %%% %%% @doc Log activities on the current connection (tool-internal use only). -%%% @see ct_logs:start_log/1 +%%% @see ct_logs:start_log/1 start_log(Heading) -> log(start_log,[Heading]). @@ -119,7 +143,7 @@ start_log(Heading) -> %%% @spec cont_log(Format,Args) -> ok %%% %%% @doc Log activities on the current connection (tool-internal use only). -%%% @see ct_logs:cont_log/2 +%%% @see ct_logs:cont_log/2 cont_log(Format,Args) -> log(cont_log,[Format,Args]). @@ -127,7 +151,7 @@ cont_log(Format,Args) -> %%% @spec end_log() -> ok %%% %%% @doc Log activities on the current connection (tool-internal use only). -%%% @see ct_logs:end_log/0 +%%% @see ct_logs:end_log/0 end_log() -> log(end_log,[]). @@ -148,10 +172,10 @@ do_within_time(Fun,Timeout) -> Silent = get(silent), TmpPid = spawn_link(fun() -> put(silent,Silent), R = Fun(), - Self ! {self(),R} + Self ! {self(),R} end), ConnPid = get(conn_pid), - receive + receive {TmpPid,Result} -> Result; {'EXIT',ConnPid,_Reason}=M -> @@ -159,7 +183,7 @@ do_within_time(Fun,Timeout) -> exit(TmpPid,kill), self() ! M, {error,connection_closed} - after + after Timeout -> exit(TmpPid,kill), receive @@ -176,12 +200,65 @@ do_within_time(Fun,Timeout) -> %%%================================================================= %%% Internal functions +do_start(Address,InitData,CallbackMod,Opts0) -> + Opts = check_opts(Opts0,#gen_opts{callback=CallbackMod, + address=Address, + init_data=InitData}), + case ct_util:does_connection_exist(Opts#gen_opts.name, + Address,CallbackMod) of + {ok,Pid} when Opts#gen_opts.use_existing -> + log("ct_gen_conn:start","Using existing connection!\n",[]), + {ok,Pid}; + {ok,Pid} when not Opts#gen_opts.use_existing -> + {error,{connection_exists,Pid}}; + false -> + do_start(Opts) + end. + +do_start(Opts) -> + Self = self(), + Pid = spawn(fun() -> init_gen(Self, Opts) end), + MRef = erlang:monitor(process,Pid), + receive + {connected,Pid} -> + erlang:demonitor(MRef, [flush]), + ct_util:register_connection(Opts#gen_opts.name, Opts#gen_opts.address, + Opts#gen_opts.callback, Pid), + {ok,Pid}; + {Error,Pid} -> + receive {'DOWN',MRef,process,_,_} -> ok end, + Error; + {'DOWN',MRef,process,_,Reason} -> + log("ct_gen_conn:start", + "Connection process died: ~p\n", + [Reason]), + {error,{connection_process_died,Reason}} + end. + +check_opts(Opts0) -> + check_opts(Opts0,#gen_opts{}). + +check_opts([{name,Name}|T],Opts) -> + check_opts(T,Opts#gen_opts{name=Name}); +check_opts([{reconnect,Bool}|T],Opts) -> + check_opts(T,Opts#gen_opts{reconnect=Bool}); +check_opts([{forward_messages,Bool}|T],Opts) -> + check_opts(T,Opts#gen_opts{forward=Bool}); +check_opts([{use_existing_connection,Bool}|T],Opts) -> + check_opts(T,Opts#gen_opts{use_existing=Bool}); +check_opts([{old,Bool}|T],Opts) -> + check_opts(T,Opts#gen_opts{old=Bool}); +check_opts([],Opts) -> + Opts. + call(Pid,Msg) -> + call(Pid,Msg,infinity). +call(Pid,Msg,Timeout) -> MRef = erlang:monitor(process,Pid), Ref = make_ref(), Pid ! {Msg,{self(),Ref}}, receive - {Ref, Result} -> + {Ref, Result} -> erlang:demonitor(MRef, [flush]), case Result of {retry,_Data} -> @@ -189,8 +266,11 @@ call(Pid,Msg) -> Other -> Other end; - {'DOWN',MRef,process,_,Reason} -> + {'DOWN',MRef,process,_,Reason} -> {error,{process_down,Pid,Reason}} + after Timeout -> + erlang:demonitor(MRef, [flush]), + exit(timeout) end. return({To,Ref},Result) -> @@ -198,36 +278,47 @@ return({To,Ref},Result) -> init_gen(Parent,Opts) -> process_flag(trap_exit,true), - CtUtilServer = whereis(ct_util_server), - link(CtUtilServer), put(silent,false), - case catch (Opts#gen_opts.callback):init(Opts#gen_opts.name, - Opts#gen_opts.address, - Opts#gen_opts.init_data) of + try (Opts#gen_opts.callback):init(Opts#gen_opts.name, + Opts#gen_opts.address, + Opts#gen_opts.init_data) of {ok,ConnPid,State} when is_pid(ConnPid) -> link(ConnPid), put(conn_pid,ConnPid), + CtUtilServer = whereis(ct_util_server), + link(CtUtilServer), Parent ! {connected,self()}, loop(Opts#gen_opts{conn_pid=ConnPid, cb_state=State, ct_util_server=CtUtilServer}); {error,Reason} -> Parent ! {{error,Reason},self()} + catch + throw:{error,Reason} -> + Parent ! {{error,Reason},self()} end. loop(Opts) -> receive {'EXIT',Pid,Reason} when Pid==Opts#gen_opts.conn_pid -> - log("Connection down!\nOpening new!","Reason: ~p\nAddress: ~p\n", - [Reason,Opts#gen_opts.address]), - case reconnect(Opts) of - {ok, NewPid, NewState} -> - link(NewPid), - put(conn_pid,NewPid), - loop(Opts#gen_opts{conn_pid=NewPid,cb_state=NewState}); - Error -> + case Opts#gen_opts.reconnect of + true -> + log("Connection down!\nOpening new!", + "Reason: ~p\nAddress: ~p\n", + [Reason,Opts#gen_opts.address]), + case reconnect(Opts) of + {ok, NewPid, NewState} -> + link(NewPid), + put(conn_pid,NewPid), + loop(Opts#gen_opts{conn_pid=NewPid,cb_state=NewState}); + Error -> + ct_util:unregister_connection(self()), + log("Reconnect failed. Giving up!","Reason: ~p\n", + [Error]) + end; + false -> ct_util:unregister_connection(self()), - log("Reconnect failed. Giving up!","Reason: ~p\n",[Error]) + log("Connection closed!","Reason: ~p\n",[Reason]) end; {'EXIT',Pid,Reason} -> case Opts#gen_opts.ct_util_server of @@ -252,24 +343,40 @@ loop(Opts) -> loop(Opts); {{retry,{_Error,_Name,_CPid,Msg}}, From} -> log("Rerunning command","Connection reestablished. Rerunning command...",[]), - {Return,NewState} = + {Return,NewState} = (Opts#gen_opts.callback):handle_msg(Msg,Opts#gen_opts.cb_state), return(From, Return), - loop(Opts#gen_opts{cb_state=NewState}); - {Msg,From={Pid,_Ref}} when is_pid(Pid) -> - {Return,NewState} = + loop(Opts#gen_opts{cb_state=NewState}); + {Msg,From={Pid,_Ref}} when is_pid(Pid), Opts#gen_opts.old==true -> + {Return,NewState} = (Opts#gen_opts.callback):handle_msg(Msg,Opts#gen_opts.cb_state), return(From, Return), - loop(Opts#gen_opts{cb_state=NewState}) + loop(Opts#gen_opts{cb_state=NewState}); + {Msg,From={Pid,_Ref}} when is_pid(Pid) -> + case (Opts#gen_opts.callback):handle_msg(Msg,From, + Opts#gen_opts.cb_state) of + {reply,Reply,NewState} -> + return(From,Reply), + loop(Opts#gen_opts{cb_state=NewState}); + {noreply,NewState} -> + loop(Opts#gen_opts{cb_state=NewState}); + {stop,Reply,NewState} -> + ct_util:unregister_connection(self()), + (Opts#gen_opts.callback):terminate(Opts#gen_opts.conn_pid, + NewState), + return(From,Reply) + end; + Msg when Opts#gen_opts.forward==true -> + case (Opts#gen_opts.callback):handle_msg(Msg,Opts#gen_opts.cb_state) of + {noreply,NewState} -> + loop(Opts#gen_opts{cb_state=NewState}); + {stop,NewState} -> + ct_util:unregister_connection(self()), + (Opts#gen_opts.callback):terminate(Opts#gen_opts.conn_pid, + NewState) + end end. -nozero({ok,S}) when is_list(S) -> - {ok,[C || C <- S, - C=/=0, - C=/=13]}; -nozero(M) -> - M. - reconnect(Opts) -> (Opts#gen_opts.callback):reconnect(Opts#gen_opts.address, Opts#gen_opts.cb_state). @@ -277,10 +384,8 @@ reconnect(Opts) -> log(Func,Args) -> case get(silent) of - true when not ?dbg-> + true when not ?dbg-> ok; _ -> apply(ct_logs,Func,Args) end. - - diff --git a/lib/common_test/src/ct_hooks.erl b/lib/common_test/src/ct_hooks.erl index 0fe6e03079..d0432b604d 100644 --- a/lib/common_test/src/ct_hooks.erl +++ b/lib/common_test/src/ct_hooks.erl @@ -192,12 +192,12 @@ call([{Hook, call_id, NextFun} | Rest], Config, Meta, Hooks) -> case lists:keyfind(NewId, #ct_hook_config.id, Hooks) of false when NextFun =:= undefined -> {Hooks ++ [NewHook], - [{NewId, call_init} | Rest]}; + Rest ++ [{NewId, call_init}]}; ExistingHook when is_tuple(ExistingHook) -> {Hooks, Rest}; _ -> {Hooks ++ [NewHook], - [{NewId, call_init}, {NewId,NextFun} | Rest]} + Rest ++ [{NewId, call_init}, {NewId,NextFun}]} end, call(resort(NewRest,NewHooks,Meta), Config, Meta, NewHooks) catch Error:Reason -> @@ -353,11 +353,10 @@ pos(Id,[_|Rest],Num) -> pos(Id,Rest,Num+1). - catch_apply(M,F,A, Default) -> try apply(M,F,A) - catch error:Reason -> + catch _:Reason -> case erlang:get_stacktrace() of %% Return the default if it was the CTH module which did not have the function. [{M,F,A,_}|_] when Reason == undef -> diff --git a/lib/common_test/src/ct_logs.erl b/lib/common_test/src/ct_logs.erl index a07a11eeb0..f942ce6f28 100644 --- a/lib/common_test/src/ct_logs.erl +++ b/lib/common_test/src/ct_logs.erl @@ -34,9 +34,10 @@ -export([set_stylesheet/2, clear_stylesheet/1]). -export([add_external_logs/1, add_link/3]). -export([make_last_run_index/0]). --export([make_all_suites_index/1, make_all_runs_index/1]). --export([get_ts_html_wrapper/3]). --export([xhtml/2, locate_default_css_file/0, make_relative/1]). +-export([make_all_suites_index/1,make_all_runs_index/1]). +-export([get_ts_html_wrapper/4]). +-export([xhtml/2, locate_priv_file/1, make_relative/1]). +-export([insert_javascript/1]). %% Logging stuff directly from testcase -export([tc_log/3, tc_log/4, tc_log_async/3, tc_print/3, tc_print/4, @@ -57,7 +58,6 @@ -define(all_runs_name, "all_runs.html"). -define(index_name, "index.html"). -define(totals_name, "totals.info"). --define(css_default, "ct_default.css"). -define(table_color1,"#ADD8E6"). -define(table_color2,"#E4F0FE"). @@ -392,7 +392,7 @@ tc_print(Category,Format,Args) -> %%% @doc Console printout from a testcase. %%% %%% <p>This function is called by <code>ct</code> when printing -%%% stuff a testcase on the user console.</p> +%%% stuff from a testcase on the user console.</p> tc_print(Category,Importance,Format,Args) -> VLvl = case ct_util:get_testdata({verbosity,Category}) of undefined -> @@ -541,26 +541,27 @@ logger(Parent, Mode, Verbosity) -> %% dir) so logs are independent of Common Test installation {ok,Cwd} = file:get_cwd(), CTPath = code:lib_dir(common_test), - CSSFileSrc = filename:join(filename:join(CTPath, "priv"), - ?css_default), - CSSFileDestTop = filename:join(Cwd, ?css_default), - CSSFileDestRun = filename:join(AbsDir, ?css_default), - case file:copy(CSSFileSrc, CSSFileDestTop) of - {error,Reason0} -> + PrivFiles = [?css_default,?jquery_script,?tablesorter_script], + PrivFilesSrc = [filename:join(filename:join(CTPath, "priv"), F) || + F <- PrivFiles], + PrivFilesDestTop = [filename:join(Cwd, F) || F <- PrivFiles], + PrivFilesDestRun = [filename:join(AbsDir, F) || F <- PrivFiles], + case copy_priv_files(PrivFilesSrc, PrivFilesDestTop) of + {error,Src1,Dest1,Reason1} -> io:format(user, "ERROR! "++ - "CSS file ~p could not be copied to ~p. "++ - "Reason: ~p~n", - [CSSFileSrc,CSSFileDestTop,Reason0]), - exit({css_file_error,CSSFileDestTop}); - _ -> - case file:copy(CSSFileSrc, CSSFileDestRun) of - {error,Reason1} -> + "Priv file ~p could not be copied to ~p. "++ + "Reason: ~p~n", + [Src1,Dest1,Reason1]), + exit({priv_file_error,Dest1}); + ok -> + case copy_priv_files(PrivFilesSrc, PrivFilesDestRun) of + {error,Src2,Dest2,Reason2} -> io:format(user, "ERROR! "++ - "CSS file ~p could not be copied to ~p. "++ - "Reason: ~p~n", - [CSSFileSrc,CSSFileDestRun,Reason1]), - exit({css_file_error,CSSFileDestRun}); - _ -> + "Priv file ~p could not be copied to ~p. "++ + "Reason: ~p~n", + [Src2,Dest2,Reason2]), + exit({priv_file_error,Dest2}); + ok -> ok end end @@ -605,6 +606,16 @@ logger(Parent, Mode, Verbosity) -> tc_groupleaders=[], async_print_jobs=[]}). +copy_priv_files([SrcF | SrcFs], [DestF | DestFs]) -> + case file:copy(SrcF, DestF) of + {error,Reason} -> + {error,SrcF,DestF,Reason}; + _ -> + copy_priv_files(SrcFs, DestFs) + end; +copy_priv_files([], []) -> + ok. + logger_loop(State) -> receive {log,SyncOrAsync,Pid,GL,Category,Importance,List} -> @@ -727,14 +738,24 @@ create_io_fun(FromPid, State) -> print_to_log(sync, FromPid, TCGL, List, State) -> IoFun = create_io_fun(FromPid, State), - io:format(TCGL, "~s", [lists:foldl(IoFun, [], List)]), + %% in some situations (exceptions), the printout is made from the + %% test server IO process and there's no valid group leader to send to + IoProc = if FromPid /= TCGL -> TCGL; + true -> State#logger_state.ct_log_fd + end, + io:format(IoProc, "~s", [lists:foldl(IoFun, [], List)]), State; print_to_log(async, FromPid, TCGL, List, State) -> IoFun = create_io_fun(FromPid, State), + %% in some situations (exceptions), the printout is made from the + %% test server IO process and there's no valid group leader to send to + IoProc = if FromPid /= TCGL -> TCGL; + true -> State#logger_state.ct_log_fd + end, Printer = fun() -> test_server:permit_io(TCGL, self()), - io:format(TCGL, "~s", [lists:foldl(IoFun, [], List)]) + io:format(IoProc, "~s", [lists:foldl(IoFun, [], List)]) end, case State#logger_state.async_print_jobs of [] -> @@ -839,7 +860,7 @@ set_evmgr_gl(GL) -> open_ctlog() -> {ok,Fd} = file:open(?ct_log_name,[write]), - io:format(Fd, header("Common Test Framework Log"), []), + io:format(Fd, header("Common Test Framework Log", {[],[1,2],[]}), []), case file:consult(ct_run:variables_file_name("../")) of {ok,Vars} -> io:format(Fd, config_table(Vars), []); @@ -1149,14 +1170,14 @@ total_row(Success, Fail, UserSkip, AutoSkip, NotBuilt, All) -> integer_to_list(UserSkip),integer_to_list(AutoSkip)} end, [xhtml("<tr valign=top>\n", - ["<tr class=\"",odd_or_even(),"\">\n"]), + ["</tbody>\n<tfoot>\n<tr class=\"",odd_or_even(),"\">\n"]), "<td><b>Total</b></td>\n", Label, TimestampCell, "<td align=right><b>",integer_to_list(Success),"<b></td>\n", "<td align=right><b>",integer_to_list(Fail),"<b></td>\n", "<td align=right>",integer_to_list(AllSkip), " (",UserSkipStr,"/",AutoSkipStr,")</td>\n", "<td align=right><b>",integer_to_list(NotBuilt),"<b></td>\n", - AllInfo, "</tr>\n"]. + AllInfo, "</tr>\n</tfoot>\n"]. not_built(_BaseName,_LogDir,_All,[]) -> 0; @@ -1213,10 +1234,12 @@ index_header(Label, StartTime) -> Head = case Label of undefined -> - header("Test Results", format_time(StartTime)); + header("Test Results", format_time(StartTime), + {[],[1],[2,3,4,5]}); _ -> header("Test Results for '" ++ Label ++ "'", - format_time(StartTime)) + format_time(StartTime), + {[],[1],[2,3,4,5]}) end, [Head | ["<center>\n", @@ -1228,15 +1251,17 @@ index_header(Label, StartTime) -> "\">COMMON TEST FRAMEWORK LOG</a>\n</div>"]), xhtml("<br>\n", "<br /><br /><br />\n"), xhtml(["<table border=\"3\" cellpadding=\"5\" " - "bgcolor=\"",?table_color3,"\">\n"], "<table>\n"), + "bgcolor=\"",?table_color3,"\">\n"], + ["<table id=\"",?sortable_table_name,"\">\n", + "<thead>\n<tr>\n"]), "<th><b>Test Name</b></th>\n", xhtml(["<th><font color=\"",?table_color3,"\">_</font>Ok" "<font color=\"",?table_color3,"\">_</font></th>\n"], "<th>Ok</th>\n"), "<th>Failed</th>\n", "<th>Skipped", xhtml("<br>", "<br />"), "(User/Auto)</th>\n" - "<th>Missing", xhtml("<br>", "<br />"), "Suites</th>\n" - "\n"]]. + "<th>Missing", xhtml("<br>", "<br />"), "Suites</th>\n", + xhtml("", "</tr>\n</thead>\n<tbody>\n")]]. all_suites_index_header() -> {ok,Cwd} = file:get_cwd(), @@ -1249,12 +1274,14 @@ all_suites_index_header(IndexDir) -> AllRunsLink = xhtml(["<a href=\"",?all_runs_name,"\">",AllRuns,"</a>\n"], ["<div id=\"button_holder\" class=\"btn\">\n" "<a href=\"",?all_runs_name,"\">",AllRuns,"</a>\n</div>"]), - [header("Test Results") | + [header("Test Results", {[3],[1,2,8,9,10],[4,5,6,7]}) | ["<center>\n", AllRunsLink, xhtml("<br><br>\n", "<br /><br />\n"), xhtml(["<table border=\"3\" cellpadding=\"5\" " - "bgcolor=\"",?table_color2,"\">\n"], "<table>\n"), + "bgcolor=\"",?table_color2,"\">\n"], + ["<table id=\"",?sortable_table_name,"\">\n", + "<thead>\n<tr>\n"]), "<th>Test Name</th>\n", "<th>Label</th>\n", "<th>Test Run Started</th>\n", @@ -1267,7 +1294,7 @@ all_suites_index_header(IndexDir) -> "<th>Node</th>\n", "<th>CT Log</th>\n", "<th>Old Runs</th>\n", - "\n"]]. + xhtml("", "</tr>\n</thead>\n<tbody>\n")]]. all_runs_header() -> {ok,Cwd} = file:get_cwd(), @@ -1279,10 +1306,12 @@ all_runs_header() -> "<a href=\"",?index_name, "\">TEST INDEX PAGE</a>\n</div>"]), xhtml("<br>\n", "<br /><br />\n")], - [header(Title) | + [header(Title, {[1],[2,3,5],[4,6,7,8,9,10]}) | ["<center>\n", IxLink, xhtml(["<table border=\"3\" cellpadding=\"5\" " - "bgcolor=\"",?table_color1,"\">\n"], "<table>\n"), + "bgcolor=\"",?table_color1,"\">\n"], + ["<table id=\"",?sortable_table_name,"\">\n", + "<thead>\n<tr>\n"]), "<th><b>History</b></th>\n" "<th><b>Node</b></th>\n" "<th><b>Label</b></th>\n" @@ -1294,23 +1323,29 @@ all_runs_header() -> "<th>Ok</th>\n"), "<th>Failed</th>\n" "<th>Skipped<br>(User/Auto)</th>\n" - "<th>Missing<br>Suites</th>\n" - "\n"]]. + "<th>Missing<br>Suites</th>\n", + xhtml("", "</tr>\n</thead>\n<tbody>\n")]]. -header(Title) -> - header1(Title, ""). -header(Title, SubTitle) -> - header1(Title, SubTitle). +header(Title, TableCols) -> + header1(Title, "", TableCols). +header(Title, SubTitle, TableCols) -> + header1(Title, SubTitle, TableCols). -header1(Title, SubTitle) -> +header1(Title, SubTitle, TableCols) -> SubTitleHTML = if SubTitle =/= "" -> ["<center>\n", "<h3>" ++ SubTitle ++ "</h3>\n", xhtml("</center>\n<br>\n", "</center>\n<br />\n")]; - true -> xhtml("<br>\n", "<br />\n") + true -> xhtml("<br>", "<br />") end, CSSFile = xhtml(fun() -> "" end, - fun() -> make_relative(locate_default_css_file()) end), + fun() -> make_relative(locate_priv_file(?css_default)) end), + JQueryFile = + xhtml(fun() -> "" end, + fun() -> make_relative(locate_priv_file(?jquery_script)) end), + TableSorterFile = + xhtml(fun() -> "" end, + fun() -> make_relative(locate_priv_file(?tablesorter_script)) end), [xhtml(["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n", "<html>\n"], ["<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n", @@ -1321,7 +1356,17 @@ header1(Title, SubTitle) -> "<title>" ++ Title ++ " " ++ SubTitle ++ "</title>\n", "<meta http-equiv=\"cache-control\" content=\"no-cache\">\n", xhtml("", - ["<link rel=\"stylesheet\" href=\"",CSSFile,"\" type=\"text/css\">"]), + ["<link rel=\"stylesheet\" href=\"",CSSFile,"\" type=\"text/css\">\n"]), + xhtml("", + ["<script type=\"text/javascript\" src=\"",JQueryFile, + "\"></script>\n"]), + xhtml("", + ["<script type=\"text/javascript\" src=\"",TableSorterFile, + "\"></script>\n"]), + xhtml(fun() -> "" end, + fun() -> insert_javascript({tablesorter,?sortable_table_name, + TableCols}) + end), "</head>\n", body_tag(), "<center>\n", @@ -1333,6 +1378,10 @@ index_footer() -> ["</table>\n" "</center>\n" | footer()]. +all_runs_index_footer() -> + ["</tbody>\n</table>\n" + "</center>\n" | footer()]. + footer() -> ["<center>\n", xhtml("<br><br>\n<hr>\n", "<br /><br />\n"), @@ -1344,7 +1393,8 @@ footer() -> xhtml("<br>\n", "<br />\n"), xhtml("</font></p>\n", "</div>\n"), "</center>\n" - "</body>\n"]. + "</body>\n" + "</html>\n"]. body_tag() -> @@ -1360,7 +1410,7 @@ current_time() -> format_time({{Y, Mon, D}, {H, Min, S}}) -> Weekday = weekday(calendar:day_of_the_week(Y, Mon, D)), - lists:flatten(io_lib:format("~s ~s ~p ~w ~2.2.0w:~2.2.0w:~2.2.0w", + lists:flatten(io_lib:format("~s ~s ~2.2.0w ~w ~2.2.0w:~2.2.0w:~2.2.0w", [Weekday, month(Mon), D, Y, H, Min, S])). weekday(1) -> "Mon"; @@ -1486,8 +1536,12 @@ config_table_header() -> [ xhtml(["<h2>Configuration</h2>\n" "<table border=\"3\" cellpadding=\"5\" bgcolor=\"",?table_color1,"\"\n"], - "<h4>CONFIGURATION</h4>\n<table>\n"), - "<tr><th>Key</th><th>Value</th></tr>\n"]. + ["<h4>CONFIGURATION</h4>\n", + "<table id=\"",?sortable_table_name,"\">\n", + "<thead>\n"]), + "<tr><th>Key</th><th>Value</th></tr>\n", + xhtml("", "</thead>\n<tbody>\n") + ]. config_table1([{Key,Value}|Vars]) -> [xhtml(["<tr><td>", atom_to_list(Key), "</td>\n", @@ -1497,7 +1551,7 @@ config_table1([{Key,Value}|Vars]) -> "<td>", io_lib:format("~p",[Value]), "</td>\n</tr>\n"]) | config_table1(Vars)]; config_table1([]) -> - ["</table>\n"]. + ["</tbody>\n</table>\n"]. make_all_runs_index(When) -> @@ -1511,7 +1565,8 @@ make_all_runs_index(When) -> DirsSorted = (catch sort_all_runs(Dirs)), Header = all_runs_header(), Index = [runentry(Dir) || Dir <- DirsSorted], - Result = file:write_file(AbsName,Header++Index++index_footer()), + Result = file:write_file(AbsName,Header++Index++ + all_runs_index_footer()), if When == start -> ok; true -> io:put_chars("done\n") end, @@ -2147,34 +2202,34 @@ basic_html() -> end. %%%----------------------------------------------------------------- -%%% @spec locate_default_css_file() -> CSSFile +%%% @spec locate_priv_file(FileName) -> PrivFile %%% %%% @doc %%% -locate_default_css_file() -> +locate_priv_file(FileName) -> {ok,CWD} = file:get_cwd(), - CSSFileInCwd = filename:join(CWD, ?css_default), - case filelib:is_file(CSSFileInCwd) of + PrivFileInCwd = filename:join(CWD, FileName), + case filelib:is_file(PrivFileInCwd) of true -> - CSSFileInCwd; + PrivFileInCwd; false -> - CSSResultFile = + PrivResultFile = case {whereis(?MODULE),self()} of {Self,Self} -> %% executed on the ct_logs process - filename:join(get(ct_run_dir), ?css_default); + filename:join(get(ct_run_dir), FileName); _ -> %% executed on other process than ct_logs {ok,RunDir} = get_log_dir(true), - filename:join(RunDir, ?css_default) + filename:join(RunDir, FileName) end, - case filelib:is_file(CSSResultFile) of + case filelib:is_file(PrivResultFile) of true -> - CSSResultFile; + PrivResultFile; false -> %% last resort, try use css file in CT installation CTPath = code:lib_dir(common_test), - filename:join(filename:join(CTPath, "priv"), ?css_default) + filename:join(filename:join(CTPath, "priv"), FileName) end end. @@ -2213,7 +2268,7 @@ make_relative1(DirTs, CwdTs) -> %%% %%% @doc %%% -get_ts_html_wrapper(TestName, PrintLabel, Cwd) -> +get_ts_html_wrapper(TestName, PrintLabel, Cwd, TableCols) -> TestName1 = if is_list(TestName) -> lists:flatten(TestName); true -> @@ -2273,17 +2328,36 @@ get_ts_html_wrapper(TestName, PrintLabel, Cwd) -> "Open Telecom Platform</a><br />\n", "Updated: <!date>", current_time(), "<!/date>", "<br />\n</div>\n"], - CSSFile = xhtml(fun() -> "" end, - fun() -> make_relative(locate_default_css_file(), Cwd) end), + CSSFile = + xhtml(fun() -> "" end, + fun() -> make_relative(locate_priv_file(?css_default), + Cwd) + end), + JQueryFile = + xhtml(fun() -> "" end, + fun() -> make_relative(locate_priv_file(?jquery_script), + Cwd) + end), + TableSorterFile = + xhtml(fun() -> "" end, + fun() -> make_relative(locate_priv_file(?tablesorter_script), + Cwd) + end), + TableSorterScript = + xhtml(fun() -> "" end, + fun() -> insert_javascript({tablesorter, + ?sortable_table_name, + TableCols}) end), {xhtml, ["<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n", "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n", "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">\n", "<head>\n<title>", TestName1, "</title>\n", "<meta http-equiv=\"cache-control\" content=\"no-cache\">\n", - "<link rel=\"stylesheet\" href=\"", CSSFile, "\" type=\"text/css\">", - "</head>\n","<body>\n", - LabelStr, "\n"], + "<link rel=\"stylesheet\" href=\"", CSSFile, "\" type=\"text/css\">\n", + "<script type=\"text/javascript\" src=\"", JQueryFile, "\"></script>\n", + "<script type=\"text/javascript\" src=\"", TableSorterFile, "\"></script>\n"] ++ + TableSorterScript ++ ["</head>\n","<body>\n", LabelStr, "\n"], ["<center>\n<br /><hr /><p>\n", "<a href=\"", AllRuns, "\">Test run history\n</a> | ", @@ -2291,3 +2365,89 @@ get_ts_html_wrapper(TestName, PrintLabel, Cwd) -> "\">Top level test index\n</a>\n</p>\n", Copyright,"</center>\n</body>\n</html>\n"]} end. + +insert_javascript({tablesorter,_TableName,undefined}) -> + []; + +insert_javascript({tablesorter,TableName, + {DateCols,TextCols,ValCols}}) -> + Headers = + lists:flatten( + lists:sort( + lists:flatmap(fun({Sorter,Cols}) -> + [lists:flatten( + io_lib:format(" ~w: " + "{ sorter: '~s' },\n", + [Col-1,Sorter])) || Col<-Cols] + end, [{"CTDateSorter",DateCols}, + {"CTTextSorter",TextCols}, + {"CTValSorter",ValCols}]))), + Headers1 = string:substr(Headers, 1, length(Headers)-2), + + ["<script type=\"text/javascript\">\n", + "// Parser for date format, e.g: Wed Jul 4 2012 11:24:15\n", + "var monthNames = {};\n", + "monthNames[\"Jan\"] = \"01\"; monthNames[\"Feb\"] = \"02\";\n", + "monthNames[\"Mar\"] = \"03\"; monthNames[\"Apr\"] = \"04\";\n", + "monthNames[\"May\"] = \"05\"; monthNames[\"Jun\"] = \"06\";\n", + "monthNames[\"Jul\"] = \"07\"; monthNames[\"Aug\"] = \"08\";\n", + "monthNames[\"Sep\"] = \"09\"; monthNames[\"Oct\"] = \"10\";\n", + "monthNames[\"Nov\"] = \"11\"; monthNames[\"Dec\"] = \"12\";\n", + "$.tablesorter.addParser({\n", + " id: 'CTDateSorter',\n", + " is: function(s) {\n", + " return false; },\n", + " format: function(s) {\n", + %% place empty cells, "-" and "?" at the bottom + " if (s.length < 2) return 999999999;\n", + " else {\n", + %% match out each date element + " var date = s.match(/(\\w{3})\\s(\\w{3})\\s(\\d{2})\\s(\\d{4})\\s(\\d{2}):(\\d{2}):(\\d{2})/);\n", + " var y = date[4]; var mo = monthNames[date[2]]; var d = String(date[3]);\n", + " var h = String(date[5]); var mi = String(date[6]); var sec = String(date[7]);\n", + " return (parseInt('' + y + mo + d + h + mi + sec)); }},\n", + " type: 'numeric' });\n", + + "// Parser for general text format\n", + "$.tablesorter.addParser({\n", + " id: 'CTTextSorter',\n", + " is: function(s) {\n", + " return false; },\n", + " format: function(s) {\n", + %% place empty cells, "?" and "-" at the bottom + " if (s.length < 1) return 'zzzzzzzz';\n", + " else if (s == \"?\") return 'zzzzzzz';\n", + " else if (s == \"-\") return 'zzzzzz';\n", + " else if (s == \"FAILED\") return 'A';\n", + " else if (s == \"SKIPPED\") return 'B';\n", + " else if (s == \"OK\") return 'C';\n", + " else return '' + s; },\n", + " type: 'text' });\n", + + "// Parser for numerical values\n", + "$.tablesorter.addParser({\n", + " id: 'CTValSorter',\n", + " is: function(s) {\n", + " return false; },\n", + " format: function(s) {\n" + %% place empty cells and "?" at the bottom + " if (s.length < 1) return '-2';\n", + " else if (s == \"?\") return '-1';\n", + %% look for skip value, eg "3 (2/1)" + " else if ((s.search(/(\\d{1,})\\s/)) >= 0) {\n", + " var num = s.match(/(\\d{1,})\\s/);\n", + %% return only the total skip value for sorting + " return (parseInt('' + num[1])); }\n", + " else if ((s.search(/(\\d{1,})\\.(\\d{3})s/)) >= 0) {\n", + " var num = s.match(/(\\d{1,})\\.(\\d{3})/);\n", + " if (num[1] == \"0\") return (parseInt('' + num[2]));\n", + " else return (parseInt('' + num[1] + num[2])); }\n", + " else return '' + s; },\n", + " type: 'numeric' });\n", + + "$(document).ready(function() {\n", + " $(\"#",TableName,"\").tablesorter({\n", + " headers: { \n", Headers1, "\n }\n });\n", + " $(\"#",TableName,"\").trigger(\"update\");\n", + " $(\"#",TableName,"\").trigger(\"appendCache\");\n", + "});\n</script>\n"]. diff --git a/lib/common_test/src/ct_master_logs.erl b/lib/common_test/src/ct_master_logs.erl index 2a951bc5cf..9e61d5b16f 100644 --- a/lib/common_test/src/ct_master_logs.erl +++ b/lib/common_test/src/ct_master_logs.erl @@ -26,6 +26,8 @@ -export([start/2, make_all_runs_index/0, log/3, nodedir/2, stop/0]). +-include("ct_util.hrl"). + -record(state, {log_fd, start_time, logdir, rundir, nodedir_ix_fd, nodes, nodedirs=[]}). @@ -33,7 +35,6 @@ -define(all_runs_name, "master_runs.html"). -define(nodedir_index_name, "index.html"). -define(details_file_name,"details.info"). --define(css_default, "ct_default.css"). -define(table_color,"lightblue"). %%%-------------------------------------------------------------------- @@ -95,29 +96,30 @@ init(Parent,LogDir,Nodes) -> put(basic_html, true); BasicHtml -> put(basic_html, BasicHtml), - %% copy stylesheet to log dir (both top dir and test run + %% copy priv files to log dir (both top dir and test run %% dir) so logs are independent of Common Test installation CTPath = code:lib_dir(common_test), - CSSFileSrc = filename:join(filename:join(CTPath, "priv"), - ?css_default), - CSSFileDestTop = filename:join(LogDir, ?css_default), - CSSFileDestRun = filename:join(RunDirAbs, ?css_default), - case file:copy(CSSFileSrc, CSSFileDestTop) of - {error,Reason0} -> + PrivFiles = [?css_default,?jquery_script,?tablesorter_script], + PrivFilesSrc = [filename:join(filename:join(CTPath, "priv"), F) || + F <- PrivFiles], + PrivFilesDestTop = [filename:join(LogDir, F) || F <- PrivFiles], + PrivFilesDestRun = [filename:join(RunDirAbs, F) || F <- PrivFiles], + case copy_priv_files(PrivFilesSrc, PrivFilesDestTop) of + {error,Src1,Dest1,Reason1} -> io:format(user, "ERROR! "++ - "CSS file ~p could not be copied to ~p. "++ + "Priv file ~p could not be copied to ~p. "++ "Reason: ~p~n", - [CSSFileSrc,CSSFileDestTop,Reason0]), - exit({css_file_error,CSSFileDestTop}); - _ -> - case file:copy(CSSFileSrc, CSSFileDestRun) of - {error,Reason1} -> + [Src1,Dest1,Reason1]), + exit({priv_file_error,Dest1}); + ok -> + case copy_priv_files(PrivFilesSrc, PrivFilesDestRun) of + {error,Src2,Dest2,Reason2} -> io:format(user, "ERROR! "++ - "CSS file ~p could not be copied to ~p. "++ + "Priv file ~p could not be copied to ~p. "++ "Reason: ~p~n", - [CSSFileSrc,CSSFileDestRun,Reason1]), - exit({css_file_error,CSSFileDestRun}); - _ -> + [Src2,Dest2,Reason2]), + exit({priv_file_error,Dest2}); + ok -> ok end end @@ -146,6 +148,16 @@ init(Parent,LogDir,Nodes) -> {N,""} end,Nodes)}). +copy_priv_files([SrcF | SrcFs], [DestF | DestFs]) -> + case file:copy(SrcF, DestF) of + {error,Reason} -> + {error,SrcF,DestF,Reason}; + _ -> + copy_priv_files(SrcFs, DestFs) + end; +copy_priv_files([], []) -> + ok. + loop(State) -> receive {log,_From,List} -> @@ -190,7 +202,7 @@ loop(State) -> open_ct_master_log(Dir) -> FullName = filename:join(Dir,?ct_master_log_name), {ok,Fd} = file:open(FullName,[write]), - io:format(Fd,header("Common Test Master Log"),[]), + io:format(Fd,header("Common Test Master Log", {[],[1,2],[]}),[]), %% maybe add config info here later io:format(Fd, config_table([]), []), io:format(Fd, @@ -216,11 +228,14 @@ config_table(Vars) -> config_table_header() -> ["<h2>Configuration</h2>\n", xhtml(["<table border=\"3\" cellpadding=\"5\" " - "bgcolor=\"",?table_color,"\"\n"], "<table>\n"), - "<tr><th>Key</th><th>Value</th></tr>\n"]. + "bgcolor=\"",?table_color,"\"\n"], + ["<table id=\"",?sortable_table_name,"\">\n", + "<thead>\n"]), + "<tr><th>Key</th><th>Value</th></tr>\n", + xhtml("", "</thead>\n<tbody>\n")]. config_table1([]) -> - ["</table>\n"]. + ["</tbody>\n</table>\n"]. int_header() -> "<div class=\"ct_internal\"><b>*** CT MASTER ~s *** ~s</b>". @@ -250,14 +265,16 @@ close_nodedir_index(Fd) -> file:close(Fd). nodedir_index_header(StartTime) -> - [header("Log Files " ++ format_time(StartTime)) | + [header("Log Files " ++ format_time(StartTime), {[],[1,2],[]}) | ["<center>\n", "<p><a href=\"",?ct_master_log_name,"\">Common Test Master Log</a></p>", xhtml(["<table border=\"3\" cellpadding=\"5\" " - "bgcolor=\"",?table_color,"\">\n"], "<table>\n"), + "bgcolor=\"",?table_color,"\">\n"], + ["<table id=\"",?sortable_table_name,"\">\n", + "<thead>\n<tr>\n"]), "<th><b>Node</b></th>\n", "<th><b>Log</b></th>\n", - "\n"]]. + xhtml("", "</tr>\n</thead>\n<tbody>\n")]]. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% All Run Index functions %%% @@ -315,14 +332,16 @@ runentry(Dir) -> "</tr>\n"]. all_runs_header() -> - [header("Master Test Runs") | + [header("Master Test Runs", {[1],[2,3],[]}) | ["<center>\n", xhtml(["<table border=\"3\" cellpadding=\"5\" " - "bgcolor=\"",?table_color,"\">\n"], "<table>\n"), + "bgcolor=\"",?table_color,"\">\n"], + ["<table id=\"",?sortable_table_name,"\">\n", + "<thead>\n<tr>\n"]), "<th><b>History</b></th>\n" "<th><b>Master Host</b></th>\n" - "<th><b>Test Nodes</b></th>\n" - "\n"]]. + "<th><b>Test Nodes</b></th>\n", + xhtml("", "</tr></thead>\n<tbody>\n")]]. timestamp(Dir) -> [S,Min,H,D,M,Y|_] = lists:reverse(string:tokens(Dir,".-_")), @@ -346,9 +365,16 @@ read_details_file(Dir) -> %%% Internal functions %%%-------------------------------------------------------------------- -header(Title) -> +header(Title, TableCols) -> CSSFile = xhtml(fun() -> "" end, - fun() -> make_relative(locate_default_css_file()) end), + fun() -> make_relative(locate_priv_file(?css_default)) end), + JQueryFile = + xhtml(fun() -> "" end, + fun() -> make_relative(locate_priv_file(?jquery_script)) end), + TableSorterFile = + xhtml(fun() -> "" end, + fun() -> make_relative(locate_priv_file(?tablesorter_script)) end), + [xhtml(["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n", "<html>\n"], ["<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n", @@ -360,6 +386,16 @@ header(Title) -> "<meta http-equiv=\"cache-control\" content=\"no-cache\">\n", xhtml("", ["<link rel=\"stylesheet\" href=\"",CSSFile,"\" type=\"text/css\">"]), + xhtml("", + ["<script type=\"text/javascript\" src=\"",JQueryFile, + "\"></script>\n"]), + xhtml("", + ["<script type=\"text/javascript\" src=\"",TableSorterFile, + "\"></script>\n"]), + xhtml(fun() -> "" end, + fun() -> ct_logs:insert_javascript({tablesorter, + ?sortable_table_name, + TableCols}) end), "</head>\n", body_tag(), "<center>\n", @@ -367,7 +403,7 @@ header(Title) -> "</center>\n"]. index_footer() -> - ["</table>\n" + ["</tbody>\n</table>\n" "</center>\n" | footer()]. footer() -> @@ -393,7 +429,7 @@ current_time() -> format_time({{Y, Mon, D}, {H, Min, S}}) -> Weekday = weekday(calendar:day_of_the_week(Y, Mon, D)), - lists:flatten(io_lib:format("~s ~s ~p ~w ~2.2.0w:~2.2.0w:~2.2.0w", + lists:flatten(io_lib:format("~s ~s ~2.2.0w ~w ~2.2.0w:~2.2.0w:~2.2.0w", [Weekday, month(Mon), D, Y, H, Min, S])). weekday(1) -> "Mon"; @@ -446,8 +482,8 @@ basic_html() -> xhtml(HTML, XHTML) -> ct_logs:xhtml(HTML, XHTML). -locate_default_css_file() -> - ct_logs:locate_default_css_file(). +locate_priv_file(File) -> + ct_logs:locate_priv_file(File). make_relative(Dir) -> ct_logs:make_relative(Dir). diff --git a/lib/common_test/src/ct_netconfc.erl b/lib/common_test/src/ct_netconfc.erl new file mode 100644 index 0000000000..d9c4a962dc --- /dev/null +++ b/lib/common_test/src/ct_netconfc.erl @@ -0,0 +1,1828 @@ +%%---------------------------------------------------------------------- +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2012. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +%%---------------------------------------------------------------------- +%% File: ct_netconfc.erl +%% +%% Description: +%% This file contains the Netconf client interface +%% +%% @author Support +%% +%% @doc Netconf client module. +%% +%% <p>The Netconf client is compliant with RFC4741 and RFC4742.</p> +%% +%% <p> For each server to test against, the following entry can be +%% added to a configuration file:</p> +%% +%% <p>`{server_id(),options()}.'</p> +%% +%% <p> The `server_id()' or an associated `target_name()' (see +%% {@link ct}) shall then be used in calls to {@link open/2}.</p> +%% +%% <p>If no configuration exists for a server, a session can still be +%% opened by calling {@link open/2} with all necessary options given +%% in the call. The first argument to {@link open/2} can then be any +%% atom.</p> +%% +%% == Logging == +%% +%% The netconf server uses the `error_logger' for logging of netconf +%% traffic. A special purpose error handler is implemented in +%% `ct_conn_log_h'. To use this error handler, add the `cth_conn_log' +%% hook in your test suite, e.g. +%% +%% ``` +%% suite() -> +%% [{ct_hooks, [{cth_conn_log, [{conn_mod(),hook_options()}]}]}]. +%%''' +%% +%% The `conn_mod()' is the name of the common_test module implementing +%% the connection protocol, e.g. `ct_netconfc'. +%% +%% The hook option `log_type' specifies the type of logging: +%% +%% <dl> +%% <dt>`raw'</dt> +%% <dd>The sent and received netconf data is logged to a separate +%% text file as is without any formatting. A link to the file is +%% added to the test case HTML log.</dd> +%% +%% <dt>`pretty'</dt> +%% <dd>The sent and received netconf data is logged to a separate +%% text file with XML data nicely indented. A link to the file is +%% added to the test case HTML log.</dd> +%% +%% <dt>`html (default)'</dt> +%% <dd>The sent and received netconf traffic is pretty printed +%% directly in the test case HTML log.</dd> +%% +%% <dt>`silent'</dt> +%% <dd>Netconf traffic is not logged.</dd> +%% </dl> +%% +%% By default, all netconf traffic is logged in one single log +%% file. However, it is possible to have different connections logged +%% in separate files. To do this, use the hook option `hosts' and +%% list the names of the servers/connections that will be used in the +%% suite. Note that the connections must be named for this to work, +%% i.e. they must be opened with {@link open/2}. +%% +%% The `hosts' option has no effect if `log_type' is set to `html' or +%% `silent'. +%% +%% The hook options can also be specified in a configuration file with +%% the configuration variable `ct_conn_log': +%% +%% ``` +%% {ct_conn_log,[{conn_mod(),hook_options()}]}. +%% ''' +%% +%% For example: +%% +%% ``` +%% {ct_conn_log,[{ct_netconfc,[{log_type,pretty}, +%% {hosts,[key_or_name()]}]}]} +%% ''' +%% +%% <b>Note</b> that hook options specified in a configuration file +%% will overwrite the hardcoded hook options in the test suite. +%% +%% === Logging example 1 === +%% +%% The following `ct_hooks' statement will cause pretty printing of +%% netconf traffic to separate logs for the connections named +%% `nc_server1' and `nc_server2'. Any other connections will be logged +%% to default netconf log. +%% +%% ``` +%% suite() -> +%% [{ct_hooks, [{cth_conn_log, [{ct_netconfc,[{log_type,pretty}}, +%% {hosts,[nc_server1,nc_server2]}]} +%% ]}]}]. +%%''' +%% +%% Connections must be opened like this: +%% +%% ``` +%% open(nc_server1,[...]), +%% open(nc_server2,[...]). +%% ''' +%% +%% === Logging example 2 === +%% +%% The following configuration file will cause raw logging of all +%% netconf traffic into one single text file. +%% +%% ``` +%% {ct_conn_log,[{ct_netconfc,[{log_type,raw}]}]}. +%% ''' +%% +%% The `ct_hooks' statement must look like this: +%% +%% ``` +%% suite() -> +%% [{ct_hooks, [{cth_conn_log, []}]}]. +%% ''' +%% +%% The same `ct_hooks' statement without the configuration file would +%% cause HTML logging of all netconf connections into the test case +%% HTML log. +%% +%% == Notifications == +%% +%% The netconf client is also compliant with RFC5277 NETCONF Event +%% Notifications, which defines a mechanism for an asynchronous +%% message notification delivery service for the netconf protocol. +%% +%% Specific functions to support this are {@link +%% create_subscription/6} and {@link get_event_streams/3}. (The +%% functions also exist with other arities.) +%% +%% @end +%%---------------------------------------------------------------------- +-module(ct_netconfc). + +-include("ct_netconfc.hrl"). +-include("ct_util.hrl"). +-include_lib("xmerl/include/xmerl.hrl"). + +%%---------------------------------------------------------------------- +%% External exports +%%---------------------------------------------------------------------- +-export([open/1, + open/2, + only_open/1, + only_open/2, + hello/1, + hello/2, + close_session/1, + close_session/2, + kill_session/2, + kill_session/3, + send/2, + send/3, + send_rpc/2, + send_rpc/3, + lock/2, + lock/3, + unlock/2, + unlock/3, + get/2, + get/3, + get_config/3, + get_config/4, + edit_config/3, + edit_config/4, + delete_config/2, + delete_config/3, + copy_config/3, + copy_config/4, + action/2, + action/3, + create_subscription/1, + create_subscription/2, + create_subscription/3, + create_subscription/4, + create_subscription/5, + create_subscription/6, + get_event_streams/2, + get_event_streams/3, + get_capabilities/1, + get_capabilities/2, + get_session_id/1, + get_session_id/2]). + +%%---------------------------------------------------------------------- +%% Exported types +%%---------------------------------------------------------------------- +-export_type([hook_options/0, + conn_mod/0, + log_type/0, + key_or_name/0, + notification/0]). + +%%---------------------------------------------------------------------- +%% Internal exports +%%---------------------------------------------------------------------- +%% ct_gen_conn callbacks +-export([init/3, + handle_msg/3, + handle_msg/2, + terminate/2, + close/1]). + +%% ct_conn_log callback +-export([format_data/2]). + +%%---------------------------------------------------------------------- +%% Internal defines +%%---------------------------------------------------------------------- +-define(APPLICATION,?MODULE). +-define(VALID_SSH_OPTS,[user, password, user_dir]). +-define(DEFAULT_STREAM,"NETCONF"). + +-define(error(ConnName,Report), + error_logger:error_report([{ct_connection,ConnName}, + {client,self()}, + {module,?MODULE}, + {line,?LINE} | + Report])). + +-define(is_timeout(T), (is_integer(T) orelse T==infinity)). +-define(is_filter(F), + (is_atom(F) orelse (is_tuple(F) andalso is_atom(element(1,F))))). +-define(is_string(S), (is_list(S) andalso is_integer(hd(S)))). + +%%---------------------------------------------------------------------- +%% Records +%%---------------------------------------------------------------------- +%% Client state +-record(state, {host, + port, + connection, % #connection + capabilities, + session_id, + msg_id = 1, + hello_status, + buff = <<>>, + pending = [], % [#pending] + event_receiver}).% pid + +%% Run-time client options. +-record(options, {ssh = [], % Options for the ssh application + host, + port = ?DEFAULT_PORT, + timeout = ?DEFAULT_TIMEOUT, + name}). + +%% Connection reference +-record(connection, {reference, % {CM,Ch} + host, + port, + name}). + +%% Pending replies from server +-record(pending, {tref, % timer ref (returned from timer:xxx) + ref, % pending ref + msg_id, + op, + caller}).% pid which sent the request + +%%---------------------------------------------------------------------- +%% Type declarations +%%---------------------------------------------------------------------- +-type client() :: handle() | server_id() | target_name(). +-type handle() :: term(). +%% An opaque reference for a connection (netconf session). See {@link +%% ct} for more information. + +-type server_id() :: atom(). +%% A `ServerId' which exists in a configuration file. +-type target_name() :: atom(). +%% A name which is associated to a `server_id()' via a +%% `require' statement or a call to {@link ct:require/2} in the +%% test suite. +-type key_or_name() :: server_id() | target_name(). + +-type options() :: [option()]. +%% Options used for setting up ssh connection to a netconf server. + +-type option() :: {ssh,host()} | {port,inet:port_number()} | {user,string()} | + {password,string()} | {user_dir,string()} | + {timeout,timeout()}. +-type host() :: inet:host_name() | inet:ip_address(). + +-type notification() :: {notification, xml_attributes(), notification_content()}. +-type notification_content() :: [event_time()|simple_xml()]. +-type event_time() :: {eventTime,xml_attributes(),[xs_datetime()]}. + +-type stream_name() :: string(). +-type streams() :: [{stream_name(),[stream_data()]}]. +-type stream_data() :: {description,string()} | + {replaySupport,string()} | + {replayLogCreationTime,string()} | + {replayLogAgedTime,string()}. +%% See XML Schema for Event Notifications found in RFC5277 for further +%% detail about the data format for the string values. + +-type hook_options() :: [hook_option()]. +%% Options that can be given to `cth_conn_log' in the `ct_hook' statement. +-type hook_option() :: {log_type,log_type()} | + {hosts,[key_or_name()]}. +-type log_type() :: raw | pretty | html | silent. +%-type error_handler() :: module(). +-type conn_mod() :: ct_netconfc. + +-type error_reason() :: term(). + +-type simple_xml() :: {xml_tag(), xml_attributes(), xml_content()} | + {xml_tag(), xml_content()} | + xml_tag(). +%% <p>This type is further described in the documentation for the +%% <tt>Xmerl</tt> application.</p> +-type xml_tag() :: atom(). +-type xml_attributes() :: [{xml_attribute_tag(),xml_attribute_value()}]. +-type xml_attribute_tag() :: atom(). +-type xml_attribute_value() :: string(). +-type xml_content() :: [simple_xml() | iolist()]. +-type xpath() :: {xpath,string()}. + +-type netconf_db() :: running | startup | candidate. +-type xs_datetime() :: string(). +%% This date and time identifyer has the same format as the XML type +%% dateTime and compliant to RFC3339. The format is +%% ```[-]CCYY-MM-DDThh:mm:ss[.s][Z|(+|-)hh:mm]''' + +%%---------------------------------------------------------------------- +%% External interface functions +%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +-spec open(Options) -> Result when + Options :: options(), + Result :: {ok,handle()} | {error,error_reason()}. +%% @doc Open a netconf session and exchange `hello' messages. +%% +%% If the server options are specified in a configuration file, or if +%% a named client is needed for logging purposes (see {@section +%% Logging}) use {@link open/2} instead. +%% +%% The opaque `handler()' reference which is returned from this +%% function is required as client identifier when calling any other +%% function in this module. +%% +%% The `timeout' option (milli seconds) is used when setting up +%% the ssh connection and when waiting for the hello message from the +%% server. It is not used for any other purposes during the lifetime +%% of the connection. +%% +%% @end +%%---------------------------------------------------------------------- +open(Options) -> + open(Options,#options{},[],true). + +%%---------------------------------------------------------------------- +-spec open(KeyOrName, ExtraOptions) -> Result when + KeyOrName :: key_or_name(), + ExtraOptions :: options(), + Result :: {ok,handle()} | {error,error_reason()}. +%% @doc Open a named netconf session and exchange `hello' messages. +%% +%% If `KeyOrName' is a configured `server_id()' or a +%% `target_name()' associated with such an ID, then the options +%% for this server will be fetched from the configuration file. +% +%% The `ExtraOptions' argument will be added to the options found in +%% the configuration file. If the same options are given, the values +%% from the configuration file will overwrite `ExtraOptions'. +%% +%% If the server is not specified in a configuration file, use {@link +%% open/1} instead. +%% +%% The opaque `handle()' reference which is returned from this +%% function can be used as client identifier when calling any other +%% function in this module. However, if `KeyOrName' is a +%% `target_name()', i.e. if the server is named via a call to +%% `ct:require/2' or a `require' statement in the test +%% suite, then this name may be used instead of the `handle()'. +%% +%% The `timeout' option (milli seconds) is used when setting up +%% the ssh connection and when waiting for the hello message from the +%% server. It is not used for any other purposes during the lifetime +%% of the connection. +%% +%% @end +%%---------------------------------------------------------------------- +open(KeyOrName, ExtraOpts) -> + open(KeyOrName, ExtraOpts, true). + +open(KeyOrName, ExtraOpts, Hello) -> + SortedExtra = lists:keysort(1,ExtraOpts), + SortedConfig = lists:keysort(1,ct:get_config(KeyOrName,[])), + AllOpts = lists:ukeymerge(1,SortedConfig,SortedExtra), + open(AllOpts,#options{name=KeyOrName},[{name,KeyOrName}],Hello). + +open(OptList,InitOptRec,NameOpt,Hello) -> + case check_options(OptList,undefined,undefined,InitOptRec) of + {Host,Port,Options} -> + case ct_gen_conn:start({Host,Port},Options,?MODULE, + NameOpt ++ [{reconnect,false}, + {use_existing_connection,false}, + {forward_messages,true}]) of + {ok,Client} when Hello==true -> + case hello(Client,Options#options.timeout) of + ok -> + {ok,Client}; + Error -> + Error + end; + Other -> + Other + end; + Error -> + Error + end. + + +%%---------------------------------------------------------------------- +-spec only_open(Options) -> Result when + Options :: options(), + Result :: {ok,handle()} | {error,error_reason()}. +%% @doc Open a netconf session, but don't send `hello'. +%% +%% As {@link open/1} but does not send a `hello' message. +%% +%% @end +%%---------------------------------------------------------------------- +only_open(Options) -> + open(Options,#options{},[],false). + +%%---------------------------------------------------------------------- +-spec only_open(KeyOrName,ExtraOptions) -> Result when + KeyOrName :: key_or_name(), + ExtraOptions :: options(), + Result :: {ok,handle()} | {error,error_reason()}. +%% @doc Open a name netconf session, but don't send `hello'. +%% +%% As {@link open/2} but does not send a `hello' message. +%% +%% @end +%%---------------------------------------------------------------------- +only_open(KeyOrName, ExtraOpts) -> + open(KeyOrName, ExtraOpts, false). + +%%---------------------------------------------------------------------- +%% @spec hello(Client) -> Result +%% @equiv hello(Client, infinity) +hello(Client) -> + hello(Client,?DEFAULT_TIMEOUT). + +%%---------------------------------------------------------------------- +-spec hello(Client,Timeout) -> Result when + Client :: handle(), + Timeout :: timeout(), + Result :: ok | {error,error_reason()}. +%% @doc Exchange `hello' messages with the server. +%% +%% Sends a `hello' message to the server and waits for the return. +%% +%% @end +%%---------------------------------------------------------------------- +hello(Client,Timeout) -> + call(Client, {hello, Timeout}). + +%%---------------------------------------------------------------------- +%% @spec get_session_id(Client) -> Result +%% @equiv get_session_id(Client, infinity) +get_session_id(Client) -> + get_session_id(Client, ?DEFAULT_TIMEOUT). + +%%---------------------------------------------------------------------- +-spec get_session_id(Client, Timeout) -> Result when + Client :: client(), + Timeout :: timeout(), + Result :: pos_integer() | {error,error_reason()}. +%% @doc Returns the session id associated with the given client. +%% +%% @end +%%---------------------------------------------------------------------- +get_session_id(Client, Timeout) -> + call(Client, get_session_id, Timeout). + +%%---------------------------------------------------------------------- +%% @spec get_capabilities(Client) -> Result +%% @equiv get_capabilities(Client, infinity) +get_capabilities(Client) -> + get_capabilities(Client, ?DEFAULT_TIMEOUT). + +%%---------------------------------------------------------------------- +-spec get_capabilities(Client, Timeout) -> Result when + Client :: client(), + Timeout :: timeout(), + Result :: [string()] | {error,error_reason()}. +%% @doc Returns the server side capabilities +%% +%% The following capability identifiers, defined in RFC 4741, can be returned: +%% +%% <ul> +%% <li>`"urn:ietf:params:netconf:base:1.0"'</li> +%% <li>`"urn:ietf:params:netconf:capability:writable-running:1.0"'</li> +%% <li>`"urn:ietf:params:netconf:capability:candidate:1.0"'</li> +%% <li>`"urn:ietf:params:netconf:capability:confirmed-commit:1.0"'</li> +%% <li>`"urn:ietf:params:netconf:capability:rollback-on-error:1.0"'</li> +%% <li>`"urn:ietf:params:netconf:capability:startup:1.0"'</li> +%% <li>`"urn:ietf:params:netconf:capability:url:1.0"'</li> +%% <li>`"urn:ietf:params:netconf:capability:xpath:1.0"'</li> +%% </ul> +%% +%% Note, additional identifiers may exist, e.g. server side namespace. +%% +%% @end +%%---------------------------------------------------------------------- +get_capabilities(Client, Timeout) -> + call(Client, get_capabilities, Timeout). + +%% @private +send(Client, SimpleXml) -> + send(Client, SimpleXml, ?DEFAULT_TIMEOUT). +%% @private +send(Client, SimpleXml, Timeout) -> + call(Client,{send, Timeout, SimpleXml}). + +%% @private +send_rpc(Client, SimpleXml) -> + send_rpc(Client, SimpleXml, ?DEFAULT_TIMEOUT). +%% @private +send_rpc(Client, SimpleXml, Timeout) -> + call(Client,{send_rpc, SimpleXml, Timeout}). + + + +%%---------------------------------------------------------------------- +%% @spec lock(Client, Target) -> Result +%% @equiv lock(Client, Target, infinity) +lock(Client, Target) -> + lock(Client, Target,?DEFAULT_TIMEOUT). + +%%---------------------------------------------------------------------- +-spec lock(Client, Target, Timeout) -> Result when + Client :: client(), + Target :: netconf_db(), + Timeout :: timeout(), + Result :: ok | {error,error_reason()}. +%% @doc Unlock configuration target. +%% +%% Which target parameters that can be used depends on if +%% `:candidate' and/or `:startup' are supported by the +%% server. If successfull, the configuration system of the device is +%% not available to other clients (Netconf, CORBA, SNMP etc). Locks +%% are intended to be short-lived. +%% +%% The operations {@link kill_session/2} or {@link kill_session/3} can +%% be used to force the release of a lock owned by another Netconf +%% session. How this is achieved by the server side is implementation +%% specific. +%% +%% @end +%%---------------------------------------------------------------------- +lock(Client, Target, Timeout) -> + call(Client,{send_rpc_op,lock,[Target],Timeout}). + +%%---------------------------------------------------------------------- +%% @spec unlock(Client, Target) -> Result +%% @equiv unlock(Client, Target, infinity) +unlock(Client, Target) -> + unlock(Client, Target,?DEFAULT_TIMEOUT). + +%%---------------------------------------------------------------------- +-spec unlock(Client, Target, Timeout) -> Result when + Client :: client(), + Target :: netconf_db(), + Timeout :: timeout(), + Result :: ok | {error,error_reason()}. +%% @doc Unlock configuration target. +%% +%% If the client earlier has aquired a lock, via {@link lock/2} or +%% {@link lock/3}, this operation release the associated lock. To be +%% able to access another target than `running', the server must +%% support `:candidate' and/or `:startup'. +%% +%% @end +%%---------------------------------------------------------------------- +unlock(Client, Target, Timeout) -> + call(Client, {send_rpc_op, unlock, [Target], Timeout}). + +%%---------------------------------------------------------------------- +%% @spec get(Client, Filter) -> Result +%% @equiv get(Client, Filter, infinity) +get(Client, Filter) -> + get(Client, Filter, ?DEFAULT_TIMEOUT). + +%%---------------------------------------------------------------------- +-spec get(Client, Filter, Timeout) -> Result when + Client :: client(), + Filter :: simple_xml() | xpath(), + Timeout :: timeout(), + Result :: {ok,simple_xml()} | {error,error_reason()}. +%% @doc Get data. +%% +%% This operation returns both configuration and state data from the +%% server. +%% +%% Filter type `xpath' can only be used if the server supports +%% `:xpath'. +%% +%% @end +%%---------------------------------------------------------------------- +get(Client, Filter, Timeout) -> + call(Client,{send_rpc_op, get, [Filter], Timeout}). + +%%---------------------------------------------------------------------- +%% @spec get_config(Client, Source, Filter) -> Result +%% @equiv get_config(Client, Source, Filter, infinity) +get_config(Client, Source, Filter) -> + get_config(Client, Source, Filter, ?DEFAULT_TIMEOUT). + +%%---------------------------------------------------------------------- +-spec get_config(Client, Source, Filter, Timeout) -> Result when + Client :: client(), + Source :: netconf_db(), + Filter :: simple_xml() | xpath(), + Timeout :: timeout(), + Result :: {ok,simple_xml()} | {error,error_reason()}. +%% @doc Get configuration data. +%% +%% To be able to access another source than `running', the server +%% must advertise `:candidate' and/or `:startup'. +%% +%% Filter type `xpath' can only be used if the server supports +%% `:xpath'. +%% +%% +%% @end +%%---------------------------------------------------------------------- +get_config(Client, Source, Filter, Timeout) -> + call(Client, {send_rpc_op, get_config, [Source, Filter], Timeout}). + +%%---------------------------------------------------------------------- +%% @spec edit_config(Client, Target, Config) -> Result +%% @equiv edit_config(Client, Target, Config, infinity) +edit_config(Client, Target, Config) -> + edit_config(Client, Target, Config, ?DEFAULT_TIMEOUT). + +%%---------------------------------------------------------------------- +-spec edit_config(Client, Target, Config, Timeout) -> Result when + Client :: client(), + Target :: netconf_db(), + Config :: simple_xml(), + Timeout :: timeout(), + Result :: ok | {error,error_reason()}. +%% @doc Edit configuration data. +%% +%% Per default only the running target is available, unless the server +%% include `:candidate' or `:startup' in its list of +%% capabilities. +%% +%% @end +%%---------------------------------------------------------------------- +edit_config(Client, Target, Config, Timeout) -> + call(Client, {send_rpc_op, edit_config, [Target,Config], Timeout}). + + +%%---------------------------------------------------------------------- +%% @spec delete_config(Client, Target) -> Result +%% @equiv delete_config(Client, Target, infinity) +delete_config(Client, Target) -> + delete_config(Client, Target, ?DEFAULT_TIMEOUT). + +%%---------------------------------------------------------------------- +-spec delete_config(Client, Target, Timeout) -> Result when + Client :: client(), + Target :: startup | candidate, + Timeout :: timeout(), + Result :: ok | {error,error_reason()}. +%% @doc Delete configuration data. +%% +%% The running configuration cannot be deleted and `:candidate' +%% or `:startup' must be advertised by the server. +%% +%% @end +%%---------------------------------------------------------------------- +delete_config(Client, Target, Timeout) when Target == startup; + Target == candidate -> + call(Client,{send_rpc_op, delete_config, [Target], Timeout}). + +%%---------------------------------------------------------------------- +%% @spec copy_config(Client, Source, Target) -> Result +%% @equiv copy_config(Client, Source, Target, infinity) +copy_config(Client, Source, Target) -> + copy_config(Client, Source, Target, ?DEFAULT_TIMEOUT). + +%%---------------------------------------------------------------------- +-spec copy_config(Client, Target, Source, Timeout) -> Result when + Client :: client(), + Target :: netconf_db(), + Source :: netconf_db(), + Timeout :: timeout(), + Result :: ok | {error,error_reason()}. +%% @doc Copy configuration data. +%% +%% Which source and target options that can be issued depends on the +%% capabilities supported by the server. I.e. `:candidate' and/or +%% `:startup' are required. +%% +%% @end +%%---------------------------------------------------------------------- +copy_config(Client, Target, Source, Timeout) -> + call(Client,{send_rpc_op, copy_config, [Target, Source], Timeout}). + +%%---------------------------------------------------------------------- +%% @spec action(Client, Action) -> Result +%% @equiv action(Client, Action, infinity) +action(Client,Action) -> + action(Client,Action,?DEFAULT_TIMEOUT). + +%%---------------------------------------------------------------------- +-spec action(Client, Action, Timeout) -> Result when + Client :: client(), + Action :: simple_xml(), + Timeout :: timeout(), + Result :: {ok,simple_xml()} | {error,error_reason()}. +%% @doc Execute an action. +%% +%% @end +%%---------------------------------------------------------------------- +action(Client,Action,Timeout) -> + call(Client,{send_rpc_op, action, [Action], Timeout}). + +%%---------------------------------------------------------------------- +create_subscription(Client) -> + create_subscription(Client,?DEFAULT_STREAM,?DEFAULT_TIMEOUT). + +create_subscription(Client,Timeout) + when ?is_timeout(Timeout) -> + create_subscription(Client,?DEFAULT_STREAM,Timeout); +create_subscription(Client,Stream) + when is_list(Stream) -> + create_subscription(Client,Stream,?DEFAULT_TIMEOUT); +create_subscription(Client,Filter) + when ?is_filter(Filter) -> + create_subscription(Client,?DEFAULT_STREAM,Filter, + ?DEFAULT_TIMEOUT). + +create_subscription(Client,Stream,Timeout) + when is_list(Stream) andalso + ?is_timeout(Timeout) -> + call(Client,{send_rpc_op,{create_subscription,self()}, + [Stream,undefined,undefined,undefined], + Timeout}); +create_subscription(Client,StartTime,StopTime) + when is_list(StartTime) andalso + is_list(StopTime) -> + create_subscription(Client,?DEFAULT_STREAM,StartTime,StopTime, + ?DEFAULT_TIMEOUT); +create_subscription(Client,Filter,Timeout) + when ?is_filter(Filter) andalso + ?is_timeout(Timeout) -> + create_subscription(Client,?DEFAULT_STREAM,Filter,Timeout); +create_subscription(Client,Stream,Filter) + when is_list(Stream) andalso + ?is_filter(Filter) -> + create_subscription(Client,Stream,Filter,?DEFAULT_TIMEOUT). + +create_subscription(Client,StartTime,StopTime,Timeout) + when is_list(StartTime) andalso + is_list(StopTime) andalso + ?is_timeout(Timeout) -> + create_subscription(Client,?DEFAULT_STREAM,StartTime,StopTime,Timeout); +create_subscription(Client,Stream,StartTime,StopTime) + when is_list(Stream) andalso + is_list(StartTime) andalso + is_list(StopTime) -> + create_subscription(Client,Stream,StartTime,StopTime,?DEFAULT_TIMEOUT); +create_subscription(Client,Filter,StartTime,StopTime) + when ?is_filter(Filter) andalso + is_list(StartTime) andalso + is_list(StopTime) -> + create_subscription(Client,?DEFAULT_STREAM,Filter, + StartTime,StopTime,?DEFAULT_TIMEOUT); +create_subscription(Client,Stream,Filter,Timeout) + when is_list(Stream) andalso + ?is_filter(Filter) andalso + ?is_timeout(Timeout) -> + call(Client,{send_rpc_op,{create_subscription,self()}, + [Stream,Filter,undefined,undefined], + Timeout}). + +create_subscription(Client,Stream,StartTime,StopTime,Timeout) + when is_list(Stream) andalso + is_list(StartTime) andalso + is_list(StopTime) andalso + ?is_timeout(Timeout) -> + call(Client,{send_rpc_op,{create_subscription,self()}, + [Stream,undefined,StartTime,StopTime], + Timeout}); +create_subscription(Client,Stream,Filter,StartTime,StopTime) + when is_list(Stream) andalso + ?is_filter(Filter) andalso + is_list(StartTime) andalso + is_list(StopTime) -> + create_subscription(Client,Stream,Filter,StartTime,StopTime,?DEFAULT_TIMEOUT). + +%%---------------------------------------------------------------------- +-spec create_subscription(Client, Stream, Filter,StartTime, StopTime, Timeout) -> + Result when + Client :: client(), + Stream :: stream_name(), + Filter :: simple_xml(), + StartTime :: xs_datetime(), + StopTime :: xs_datetime(), + Timeout :: timeout(), + Result :: ok | {error,error_reason()}. +%% @doc Create a subscription for event notifications. +%% +%% This function sets up a subscription for netconf event +%% notifications of the given stream type, matching the given +%% filter. The calling process will receive notifications as messages +%% of type `notification()'. +%% +%% <dl> +%% <dt>Stream:</dt> +%% <dd> An optional parameter that indicates which stream of events +%% is of interest. If not present, events in the default NETCONF +%% stream will be sent.</dd> +%% +%% <dt>Filter:</dt> +%% <dd>An optional parameter that indicates which subset of all +%% possible events is of interest. The format of this parameter is +%% the same as that of the filter parameter in the NETCONF protocol +%% operations. If not present, all events not precluded by other +%% parameters will be sent. See section 3.6 for more information on +%% filters.</dd> +%% +%% <dt>StartTime:</dt> +%% <dd>An optional parameter used to trigger the replay feature and +%% indicate that the replay should start at the time specified. If +%% `StartTime' is not present, this is not a replay subscription. +%% It is not valid to specify start times that are later than the +%% current time. If the `StartTime' specified is earlier than the +%% log can support, the replay will begin with the earliest +%% available notification. This parameter is of type dateTime and +%% compliant to [RFC3339]. Implementations must support time +%% zones.</dd> +%% +%% <dt>StopTime:</dt> +%% <dd>An optional parameter used with the optional replay feature +%% to indicate the newest notifications of interest. If `StopTime' +%% is not present, the notifications will continue until the +%% subscription is terminated. Must be used with and be later than +%% `StartTime'. Values of `StopTime' in the future are valid. This +%% parameter is of type dateTime and compliant to [RFC3339]. +%% Implementations must support time zones.</dd> +%% </dl> +%% +%% See RFC5277 for further details about the event notification +%% mechanism. +%% +%% @end +%%---------------------------------------------------------------------- +create_subscription(Client,Stream,Filter,StartTime,StopTime,Timeout) -> + call(Client,{send_rpc_op,{create_subscription, self()}, + [Stream,Filter,StartTime,StopTime], + Timeout}). + +%%---------------------------------------------------------------------- +%% @spec get_event_streams(Client, Timeout) -> Result +%% @equiv get_event_streams(Client, [], Timeout) +get_event_streams(Client,Timeout) when is_integer(Timeout); Timeout==infinity -> + get_event_streams(Client,[],Timeout); + +%%---------------------------------------------------------------------- +%% @spec get_event_streams(Client, Streams) -> Result +%% @equiv get_event_streams(Client, Streams, infinity) +get_event_streams(Client,Streams) when is_list(Streams) -> + get_event_streams(Client,Streams,?DEFAULT_TIMEOUT). + +%%---------------------------------------------------------------------- +-spec get_event_streams(Client, Streams, Timeout) + -> Result when + Client :: client(), + Streams :: [stream_name()], + Timeout :: timeout(), + Result :: {ok,streams()} | {error,error_reason()}. +%% @doc Send a request to get the given event streams. +%% +%% `Streams' is a list of stream names. The following filter will +%% be sent to the netconf server in a `get' request: +%% +%% ``` +%% <netconf xmlns="urn:ietf:params:xml:ns:netmod:notification"> +%% <streams> +%% <stream> +%% <name>StreamName1</name> +%% </stream> +%% <stream> +%% <name>StreamName2</name> +%% </stream> +%% ... +%% </streams> +%% </netconf> +%% ''' +%% +%% If `Streams' is an empty list, ALL streams will be requested +%% by sending the following filter: +%% +%% ``` +%% <netconf xmlns="urn:ietf:params:xml:ns:netmod:notification"> +%% <streams/> +%% </netconf> +%% ''' +%% +%% If more complex filtering is needed, a use {@link get/2} or {@link +%% get/3} and specify the exact filter according to XML Schema for +%% Event Notifications found in RFC5277. +%% +%% @end +%%---------------------------------------------------------------------- +get_event_streams(Client,Streams,Timeout) -> + call(Client,{get_event_streams,Streams,Timeout}). + + +%%---------------------------------------------------------------------- +%% @spec close_session(Client) -> Result +%% @equiv close_session(Client, infinity) +close_session(Client) -> + close_session(Client, ?DEFAULT_TIMEOUT). + +%%---------------------------------------------------------------------- +-spec close_session(Client, Timeout) -> Result when + Client :: client(), + Timeout :: timeout(), + Result :: ok | {error,error_reason()}. +%% @doc Request graceful termination of the session associated with the client. +%% +%% When a netconf server receives a `close-session' request, it +%% will gracefully close the session. The server will release any +%% locks and resources associated with the session and gracefully +%% close any associated connections. Any NETCONF requests received +%% after a `close-session' request will be ignored. +%% +%% @end +%%---------------------------------------------------------------------- +close_session(Client, Timeout) -> + call(Client,{send_rpc_op, close_session, [], Timeout}). + + +%%---------------------------------------------------------------------- +%% @spec kill_session(Client, SessionId) -> Result +%% @equiv kill_session(Client, SessionId, infinity) +kill_session(Client, SessionId) -> + kill_session(Client, SessionId, ?DEFAULT_TIMEOUT). + +%%---------------------------------------------------------------------- +-spec kill_session(Client, SessionId, Timeout) -> Result when + Client :: client(), + SessionId :: pos_integer(), + Timeout :: timeout(), + Result :: ok | {error,error_reason()}. +%% @doc Force termination of the session associated with the supplied +%% session id. +%% +%% The server side shall abort any operations currently in process, +%% release any locks and resources associated with the session, and +%% close any associated connections. +%% +%% Only if the server is in the confirmed commit phase, the +%% configuration will be restored to its state before entering the +%% confirmed commit phase. Otherwise, no configuration roll back will +%% be performed. +%% +%% If the given `SessionId' is equal to the current session id, +%% an error will be returned. +%% +%% @end +%% ---------------------------------------------------------------------- +kill_session(Client, SessionId, Timeout) -> + call(Client,{send_rpc_op, kill_session, [SessionId], Timeout}). + + +%%---------------------------------------------------------------------- +%% Callback functions +%%---------------------------------------------------------------------- + +%% @private +init(_KeyOrName,{_Host,_Port},Options) -> + case ssh_open(Options) of + {ok, Connection} -> + log(Connection,open), + {ConnPid,_} = Connection#connection.reference, + {ok, ConnPid, #state{connection = Connection}}; + {error,Reason}-> + {error,Reason} + end. + +%% @private +terminate(_, #state{connection=Connection}) -> + ssh_close(Connection), + log(Connection,close), + ok. + +%% @private +handle_msg({hello,Timeout}, From, + #state{connection=Connection,hello_status=HelloStatus} = State) -> + case do_send(Connection, client_hello()) of + ok -> + case HelloStatus of + undefined -> + {Ref,TRef} = set_request_timer(Timeout), + {noreply, State#state{hello_status=#pending{tref=TRef, + ref=Ref, + caller=From}}}; + received -> + {reply, ok, State#state{hello_status=done}}; + {error,Reason} -> + {stop, {error,Reason}, State} + end; + Error -> + {stop, Error, State} + end; +handle_msg(_, _From, #state{session_id=undefined} = State) -> + %% Hello is not yet excanged - this shall never happen + {reply,{error,waiting_for_hello},State}; +handle_msg(get_capabilities, _From, #state{capabilities = Caps} = State) -> + {reply, Caps, State}; +handle_msg(get_session_id, _From, #state{session_id = Id} = State) -> + {reply, Id, State}; +handle_msg({send, Timeout, SimpleXml}, From, + #state{connection=Connection,pending=Pending} = State) -> + case do_send(Connection, SimpleXml) of + ok -> + {Ref,TRef} = set_request_timer(Timeout), + {noreply, State#state{pending=[#pending{tref=TRef, + ref=Ref, + caller=From} | Pending]}}; + Error -> + {reply, Error, State} + end; +handle_msg({send_rpc, SimpleXml, Timeout}, From, State) -> + do_send_rpc(undefined, SimpleXml, Timeout, From, State); +handle_msg({send_rpc_op, Op, Data, Timeout}, From, State) -> + SimpleXml = encode_rpc_operation(Op,Data), + do_send_rpc(Op, SimpleXml, Timeout, From, State); +handle_msg({get_event_streams=Op,Streams,Timeout}, From, State) -> + Filter = {netconf,?NETMOD_NOTIF_NAMESPACE_ATTR, + [{streams,[{stream,[{name,[Name]}]} || Name <- Streams]}]}, + SimpleXml = encode_rpc_operation(get,[Filter]), + do_send_rpc(Op, SimpleXml, Timeout, From, State). + +handle_msg({ssh_cm, _CM, {data, _Ch, _Type, Data}}, State) -> + handle_data(Data, State); +handle_msg({ssh_cm, _CM, {closed,_Ch}}, State) -> + %% This will happen if the server terminates the connection, as in + %% kill-session (or if ssh:close is called from somewhere + %% unexpected). + + %%! Log this?? - i.e. as server closing the connection + %%! Currently the log will say that the client closed the + %%! connection - due to terminate/2 + + {stop, State}; +handle_msg({Ref,timeout}, + #state{hello_status=#pending{ref=Ref,caller=Caller}} = State) -> + ct_gen_conn:return(Caller,{error,{hello_session_failed,timeout}}), + {stop,State#state{hello_status={error,timeout}}}; +handle_msg({Ref,timeout},#state{pending=Pending} = State) -> + {value,#pending{caller=Caller},Pending1} = + lists:keytake(Ref,#pending.ref,Pending), + ct_gen_conn:return(Caller,{error,timeout}), + {noreply,State#state{pending=Pending1}}. + +%% @private +%% Called by ct_util_server to close registered connections before terminate. +close(Client) -> + case get_handle(Client) of + {ok,Pid} -> + case ct_gen_conn:stop(Pid) of + {error,{process_down,Pid,noproc}} -> + {error,already_closed}; + Result -> + Result + end; + Error -> + Error + end. + + +%%---------------------------------------------------------------------- +%% Internal functions +%%---------------------------------------------------------------------- +call(Client, Msg) -> + call(Client, Msg, infinity). +call(Client, Msg, Timeout) -> + case get_handle(Client) of + {ok,Pid} -> + case ct_gen_conn:call(Pid,Msg,Timeout) of + {error,{process_down,Client,noproc}} -> + {error,no_such_client}; + {error,{process_down,Client,normal}} -> + {error,closed}; + {error,{process_down,Client,Reason}} -> + {error,{closed,Reason}}; + Other -> + Other + end; + Error -> + Error + end. + +get_handle(Client) when is_pid(Client) -> + {ok,Client}; +get_handle(Client) -> + case ct_util:get_connections(Client, ?MODULE) of + {ok,[{Pid,_}]} -> + {ok,Pid}; + {ok,[]} -> + {error,{no_connection_found,Client}}; + {ok,Conns} -> + {error,{multiple_connections_found,Client,Conns}}; + Error -> + Error + end. + +check_options([], undefined, _Port, _Options) -> + {error, no_host_address}; +check_options([], _Host, undefined, _Options) -> + {error, no_port}; +check_options([], Host, Port, Options) -> + {Host,Port,Options}; +check_options([{ssh, Host}|T], _, Port, #options{} = Options) -> + check_options(T, Host, Port, Options#options{host=Host}); +check_options([{port,Port}|T], Host, _, #options{} = Options) -> + check_options(T, Host, Port, Options#options{port=Port}); +check_options([{timeout, Timeout}|T], Host, Port, Options) + when is_integer(Timeout); Timeout==infinity -> + check_options(T, Host, Port, Options#options{timeout = Timeout}); +check_options([{X,_}=Opt|T], Host, Port, #options{ssh=SshOpts}=Options) -> + case lists:member(X,?VALID_SSH_OPTS) of + true -> + check_options(T, Host, Port, Options#options{ssh=[Opt|SshOpts]}); + false -> + {error, {invalid_option, Opt}} + end. + +%%%----------------------------------------------------------------- +set_request_timer(infinity) -> + {undefined,undefined}; +set_request_timer(T) -> + Ref = make_ref(), + {ok,TRef} = timer:send_after(T,{Ref,timeout}), + {Ref,TRef}. + + +%%%----------------------------------------------------------------- +client_hello() -> + {hello, ?NETCONF_NAMESPACE_ATTR, + [{capabilities, + [{capability,[?NETCONF_BASE_CAP++?NETCONF_BASE_CAP_VSN]}]}]}. + +%%%----------------------------------------------------------------- + +encode_rpc_operation(Lock,[Target]) when Lock==lock; Lock==unlock -> + {Lock,[{target,[Target]}]}; +encode_rpc_operation(get,[Filter]) -> + {get,filter(Filter)}; +encode_rpc_operation(get_config,[Source,Filter]) -> + {'get-config',[{source,[Source]}] ++ filter(Filter)}; +encode_rpc_operation(edit_config,[Target,Config]) -> + {'edit-config',[{target,[Target]},{config,[Config]}]}; +encode_rpc_operation(delete_config,[Target]) -> + {'delete-config',[{target,[Target]}]}; +encode_rpc_operation(copy_config,[Target,Source]) -> + {'copy-config',[{target,[Target]},{source,[Source]}]}; +encode_rpc_operation(action,[Action]) -> + {action,?ACTION_NAMESPACE_ATTR,[{data,[Action]}]}; +encode_rpc_operation(kill_session,[SessionId]) -> + {'kill-session',[{'session-id',[integer_to_list(SessionId)]}]}; +encode_rpc_operation(close_session,[]) -> + 'close-session'; +encode_rpc_operation({create_subscription,_}, + [Stream,Filter,StartTime,StopTime]) -> + {'create-subscription',?NETCONF_NOTIF_NAMESPACE_ATTR, + [{stream,[Stream]}] ++ + filter(Filter) ++ + maybe_element(startTime,StartTime) ++ + maybe_element(stopTime,StopTime)}. + +filter(undefined) -> + []; +filter({xpath,Filter}) when ?is_string(Filter) -> + [{filter,[{type,"xpath"},{select, Filter}],[]}]; +filter(Filter) -> + [{filter,[{type,"subtree"}],[Filter]}]. + +maybe_element(_,undefined) -> + []; +maybe_element(Tag,Value) -> + [{Tag,[Value]}]. + +%%%----------------------------------------------------------------- +%%% Send XML data to server +do_send_rpc(PendingOp,SimpleXml,Timeout,Caller, + #state{connection=Connection,msg_id=MsgId,pending=Pending} = State) -> + case do_send_rpc(Connection, MsgId, SimpleXml) of + ok -> + {Ref,TRef} = set_request_timer(Timeout), + {noreply, State#state{msg_id=MsgId+1, + pending=[#pending{tref=TRef, + ref=Ref, + msg_id=MsgId, + op=PendingOp, + caller=Caller} | Pending]}}; + Error -> + {reply, Error, State#state{msg_id=MsgId+1}} + end. + +do_send_rpc(Connection, MsgId, SimpleXml) -> + do_send(Connection, + {rpc, + [{'message-id',MsgId} | ?NETCONF_NAMESPACE_ATTR], + [SimpleXml]}). + +do_send(Connection, SimpleXml) -> + Xml=to_xml_doc(SimpleXml), + log(Connection,send,Xml), + ssh_send(Connection, Xml). + +to_xml_doc(Simple) -> + Prolog = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>", + Xml = list_to_binary(xmerl:export_simple([Simple], + xmerl_xml, + [#xmlAttribute{name=prolog, + value=Prolog}])), + <<Xml/binary,?END_TAG/binary>>. + +%%%----------------------------------------------------------------- +%%% Parse and handle received XML data +handle_data(NewData,#state{connection=Connection,buff=Buff} = State) -> + log(Connection,recv,NewData), + Data = <<Buff/binary,NewData/binary>>, + case xmerl_sax_parser:stream(<<>>, + [{continuation_fun,fun sax_cont/1}, + {continuation_state,{Data,Connection,false}}, + {event_fun,fun sax_event/3}, + {event_state,[]}]) of + {ok, Simple, Rest} -> + decode(Simple,State#state{buff=Rest}); + {fatal_error,_Loc,Reason,_EndTags,_EventState} -> + ?error(Connection#connection.name,[{parse_error,Reason}, + {data,Data}]), + case Reason of + {could_not_fetch_data,Msg} -> + handle_msg(Msg,State#state{buff = <<>>}); + _Other -> + Pending1 = + case State#state.pending of + [] -> + []; + Pending -> + %% Assuming the first request gets the + %% first answer + P=#pending{tref=TRef,caller=Caller} = + lists:last(Pending), + timer:cancel(TRef), + Reason1 = {failed_to_parse_received_data,Reason}, + ct_gen_conn:return(Caller,{error,Reason1}), + lists:delete(P,Pending) + end, + {noreply,State#state{pending=Pending1,buff = <<>>}} + end + end. + +%%%----------------------------------------------------------------- +%%% Parsing of XML data +%% Contiuation function for the sax parser +sax_cont(done) -> + {<<>>,done}; +sax_cont({Data,Connection,false}) -> + case binary:split(Data,[?END_TAG],[]) of + [All] -> + %% No end tag found. Remove what could be a part + %% of an end tag from the data and save for next + %% iteration + SafeSize = size(All)-5, + <<New:SafeSize/binary,Save:5/binary>> = All, + {New,{Save,Connection,true}}; + [_Msg,_Rest]=Msgs -> + %% We have at least one full message. Any excess data will + %% be returned from xmerl_sax_parser:stream/2 in the Rest + %% parameter. + {list_to_binary(Msgs),done} + end; +sax_cont({Data,Connection,true}) -> + case ssh_receive_data() of + {ok,Bin} -> + log(Connection,recv,Bin), + sax_cont({<<Data/binary,Bin/binary>>,Connection,false}); + {error,Reason} -> + throw({could_not_fetch_data,Reason}) + end. + + + +%% Event function for the sax parser. It builds a simple XML structure. +%% Care is taken to keep namespace attributes and prefixes as in the original XML. +sax_event(Event,_Loc,State) -> + sax_event(Event,State). + +sax_event({startPrefixMapping, Prefix, Uri},Acc) -> + %% startPrefixMapping will always come immediately before the + %% startElement where the namespace is defined. + [{xmlns,{Prefix,Uri}}|Acc]; +sax_event({startElement,_Uri,_Name,QN,Attrs},Acc) -> + %% Pick out any namespace attributes inserted due to a + %% startPrefixMapping event.The rest of Acc will then be only + %% elements. + {NsAttrs,NewAcc} = split_attrs_and_elements(Acc,[]), + Tag = qn_to_tag(QN), + [{Tag,NsAttrs ++ parse_attrs(Attrs),[]}|NewAcc]; +sax_event({endElement,_Uri,_Name,_QN},[{Name,Attrs,Cont},{Parent,PA,PC}|Acc]) -> + [{Parent,PA,[{Name,Attrs,lists:reverse(Cont)}|PC]}|Acc]; +sax_event(endDocument,[{Tag,Attrs,Cont}]) -> + {Tag,Attrs,lists:reverse(Cont)}; +sax_event({characters,String},[{Name,Attrs,Cont}|Acc]) -> + [{Name,Attrs,[String|Cont]}|Acc]; +sax_event(_Event,State) -> + State. + +split_attrs_and_elements([{xmlns,{Prefix,Uri}}|Rest],Attrs) -> + split_attrs_and_elements(Rest,[{xmlnstag(Prefix),Uri}|Attrs]); +split_attrs_and_elements(Elements,Attrs) -> + {Attrs,Elements}. + +xmlnstag([]) -> + xmlns; +xmlnstag(Prefix) -> + list_to_atom("xmlns:"++Prefix). + +qn_to_tag({[],Name}) -> + list_to_atom(Name); +qn_to_tag({Prefix,Name}) -> + list_to_atom(Prefix ++ ":" ++ Name). + +parse_attrs([{_Uri, [], Name, Value}|Attrs]) -> + [{list_to_atom(Name),Value}|parse_attrs(Attrs)]; +parse_attrs([{_Uri, Prefix, Name, Value}|Attrs]) -> + [{list_to_atom(Prefix ++ ":" ++ Name),Value}|parse_attrs(Attrs)]; +parse_attrs([]) -> + []. + + +%%%----------------------------------------------------------------- +%%% Decoding of parsed XML data +decode({Tag,Attrs,_}=E, #state{connection=Connection,pending=Pending}=State) -> + ConnName = Connection#connection.name, + case get_local_name_atom(Tag) of + 'rpc-reply' -> + case get_msg_id(Attrs) of + undefined -> + case Pending of + [#pending{msg_id=MsgId}] -> + ?error(ConnName,[{warning,rpc_reply_missing_msg_id}, + {assuming,MsgId}]), + decode_rpc_reply(MsgId,E,State); + _ -> + ?error(ConnName,[{error,rpc_reply_missing_msg_id}]), + {noreply,State} + end; + MsgId -> + decode_rpc_reply(MsgId,E,State) + end; + hello -> + case State#state.hello_status of + undefined -> + case decode_hello(E) of + {ok,SessionId,Capabilities} -> + {noreply,State#state{session_id = SessionId, + capabilities = Capabilities, + hello_status = received}}; + {error,Reason} -> + {noreply,State#state{hello_status = {error,Reason}}} + end; + #pending{tref=TRef,caller=Caller} -> + timer:cancel(TRef), + case decode_hello(E) of + {ok,SessionId,Capabilities} -> + ct_gen_conn:return(Caller,ok), + {noreply,State#state{session_id = SessionId, + capabilities = Capabilities, + hello_status = done}}; + {error,Reason} -> + ct_gen_conn:return(Caller,{error,Reason}), + {stop,State#state{hello_status={error,Reason}}} + end; + Other -> + ?error(ConnName,[{got_unexpected_hello,E}, + {hello_status,Other}]), + {noreply,State} + end; + notification -> + EventReceiver = State#state.event_receiver, + EventReceiver ! E, + {noreply,State}; + Other -> + %% Result of send/2, when not sending an rpc request - or + %% if netconf server sends noise. Can handle this only if + %% there is just one pending that matches (i.e. has + %% undefined msg_id and op) + case [P || P = #pending{msg_id=undefined,op=undefined} <- Pending] of + [#pending{tref=TRef, + caller=Caller}] -> + timer:cancel(TRef), + ct_gen_conn:return(Caller,E), + {noreply,State#state{pending=[]}}; + _ -> + ?error(ConnName,[{got_unexpected_msg,Other}, + {expecting,Pending}]), + {noreply,State} + end + + end. + +get_msg_id(Attrs) -> + case lists:keyfind('message-id',1,Attrs) of + {_,Str} -> + list_to_integer(Str); + false -> + undefined + end. + +decode_rpc_reply(MsgId,{_,Attrs,Content0}=E,#state{pending=Pending} = State) -> + case lists:keytake(MsgId,#pending.msg_id,Pending) of + {value, #pending{tref=TRef,op=Op,caller=Caller}, Pending1} -> + timer:cancel(TRef), + Content = forward_xmlns_attr(Attrs,Content0), + {CallerReply,{ServerReply,State2}} = + do_decode_rpc_reply(Op,Content,State#state{pending=Pending1}), + ct_gen_conn:return(Caller,CallerReply), + {ServerReply,State2}; + false -> + %% Result of send/2, when receiving a correct + %% rpc-reply. Can handle this only if there is just one + %% pending that matches (i.e. has undefined msg_id and op) + case [P || P = #pending{msg_id=undefined,op=undefined} <- Pending] of + [#pending{tref=TRef, + msg_id=undefined, + op=undefined, + caller=Caller}] -> + timer:cancel(TRef), + ct_gen_conn:return(Caller,E), + {noreply,State#state{pending=[]}}; + _ -> + ConnName = (State#state.connection)#connection.name, + ?error(ConnName,[{got_unexpected_msg_id,MsgId}, + {expecting,Pending}]), + {noreply,State} + end + end. + +do_decode_rpc_reply(Op,Result,State) + when Op==lock; Op==unlock; Op==edit_config; Op==delete_config; + Op==copy_config; Op==kill_session -> + {decode_ok(Result),{noreply,State}}; +do_decode_rpc_reply(Op,Result,State) + when Op==get; Op==get_config; Op==action -> + {decode_data(Result),{noreply,State}}; +do_decode_rpc_reply(close_session,Result,State) -> + case decode_ok(Result) of + ok -> {ok,{stop,State}}; + Other -> {Other,{noreply,State}} + end; +do_decode_rpc_reply({create_subscription,Caller},Result,State) -> + case decode_ok(Result) of + ok -> + {ok,{noreply,State#state{event_receiver=Caller}}}; + Other -> + {Other,{noreply,State}} + end; +do_decode_rpc_reply(get_event_streams,Result,State) -> + {decode_streams(decode_data(Result)),{noreply,State}}; +do_decode_rpc_reply(undefined,Result,State) -> + {Result,{noreply,State}}. + + + +decode_ok([{Tag,Attrs,Content}]) -> + case get_local_name_atom(Tag) of + ok -> + ok; + 'rpc-error' -> + {error,forward_xmlns_attr(Attrs,Content)}; + _Other -> + {error,{unexpected_rpc_reply,[{Tag,Attrs,Content}]}} + end; +decode_ok(Other) -> + {error,{unexpected_rpc_reply,Other}}. + +decode_data([{Tag,Attrs,Content}]) -> + case get_local_name_atom(Tag) of + data -> + %% Since content of data has nothing from the netconf + %% namespace, we remove the parent's xmlns attribute here + %% - just to make the result cleaner + {ok,forward_xmlns_attr(remove_xmlnsattr_for_tag(Tag,Attrs),Content)}; + 'rpc-error' -> + {error,forward_xmlns_attr(Attrs,Content)}; + _Other -> + {error,{unexpected_rpc_reply,[{Tag,Attrs,Content}]}} + end; +decode_data(Other) -> + {error,{unexpected_rpc_reply,Other}}. + +get_qualified_name(Tag) -> + case string:tokens(atom_to_list(Tag),":") of + [TagStr] -> {[],TagStr}; + [PrefixStr,TagStr] -> {PrefixStr,TagStr} + end. + +get_local_name_atom(Tag) -> + {_,TagStr} = get_qualified_name(Tag), + list_to_atom(TagStr). + + +%% Remove the xmlns attr that points to the tag. I.e. if the tag has a +%% prefix, remove {'xmlns:prefix',_}, else remove default {xmlns,_}. +remove_xmlnsattr_for_tag(Tag,Attrs) -> + {Prefix,_TagStr} = get_qualified_name(Tag), + XmlnsTag = xmlnstag(Prefix), + case lists:keytake(XmlnsTag,1,Attrs) of + {value,_,NoNsAttrs} -> + NoNsAttrs; + false -> + Attrs + end. + +%% Take all xmlns attributes from the parent's attribute list and +%% forward into all childrens' attribute lists. But do not overwrite +%% any. +forward_xmlns_attr(ParentAttrs,Children) -> + do_forward_xmlns_attr(get_all_xmlns_attrs(ParentAttrs,[]),Children). + +do_forward_xmlns_attr(XmlnsAttrs,[{ChT,ChA,ChC}|Children]) -> + ChA1 = add_xmlns_attrs(XmlnsAttrs,ChA), + [{ChT,ChA1,ChC} | do_forward_xmlns_attr(XmlnsAttrs,Children)]; +do_forward_xmlns_attr(_XmlnsAttrs,[]) -> + []. + +add_xmlns_attrs([{Key,_}=A|XmlnsAttrs],ChA) -> + case lists:keymember(Key,1,ChA) of + true -> + add_xmlns_attrs(XmlnsAttrs,ChA); + false -> + add_xmlns_attrs(XmlnsAttrs,[A|ChA]) + end; +add_xmlns_attrs([],ChA) -> + ChA. + +get_all_xmlns_attrs([{xmlns,_}=Default|Attrs],XmlnsAttrs) -> + get_all_xmlns_attrs(Attrs,[Default|XmlnsAttrs]); +get_all_xmlns_attrs([{Key,_}=Attr|Attrs],XmlnsAttrs) -> + case atom_to_list(Key) of + "xmlns:"++_Prefix -> + get_all_xmlns_attrs(Attrs,[Attr|XmlnsAttrs]); + _ -> + get_all_xmlns_attrs(Attrs,XmlnsAttrs) + end; +get_all_xmlns_attrs([],XmlnsAttrs) -> + XmlnsAttrs. + + +%% Decode server hello to pick out session id and capabilities +decode_hello({hello,_Attrs,Hello}) -> + case lists:keyfind('session-id',1,Hello) of + {'session-id',_,[SessionId]} -> + case lists:keyfind(capabilities,1,Hello) of + {capabilities,_,Capabilities} -> + case decode_caps(Capabilities,[],false) of + {ok,Caps} -> + {ok,list_to_integer(SessionId),Caps}; + Error -> + Error + end; + false -> + {error,{incorrect_hello,capabilities_not_found}} + end; + false -> + {error,{incorrect_hello,no_session_id_found}} + end. + +decode_caps([{capability,[],[?NETCONF_BASE_CAP++Vsn=Cap]} |Caps], Acc, _) -> + case Vsn of + ?NETCONF_BASE_CAP_VSN -> + decode_caps(Caps, [Cap|Acc], true); + _ -> + {error,{incompatible_base_capability_vsn,Vsn}} + end; +decode_caps([{capability,[],[Cap]}|Caps],Acc,Base) -> + decode_caps(Caps,[Cap|Acc],Base); +decode_caps([H|_T],_,_) -> + {error,{unexpected_capability_element,H}}; +decode_caps([],_,false) -> + {error,{incorrect_hello,no_base_capability_found}}; +decode_caps([],Acc,true) -> + {ok,lists:reverse(Acc)}. + + +%% Return a list of {Name,Data}, where data is a {Tag,Value} list for each stream +decode_streams({error,Reason}) -> + {error,Reason}; +decode_streams({ok,[{netconf,_,Streams}]}) -> + {ok,decode_streams(Streams)}; +decode_streams([{streams,_,Streams}]) -> + decode_streams(Streams); +decode_streams([{stream,_,Stream} | Streams]) -> + {name,_,[Name]} = lists:keyfind(name,1,Stream), + [{Name,[{Tag,Value} || {Tag,_,[Value]} <- Stream, Tag /= name]} + | decode_streams(Streams)]; +decode_streams([]) -> + []. + + +%%%----------------------------------------------------------------- +%%% Logging + +log(Connection,Action) -> + log(Connection,Action,<<>>). +log(#connection{host=Host,port=Port,name=Name},Action,Data) -> + error_logger:info_report(#conn_log{client=self(), + address={Host,Port}, + name=Name, + action=Action, + module=?MODULE}, + Data). + + +%% Log callback - called from the error handler process +format_data(raw,Data) -> + io_lib:format("~n~s~n",[hide_password(Data)]); +format_data(pretty,Data) -> + io_lib:format("~n~s~n",[indent(Data)]); +format_data(html,Data) -> + io_lib:format("~n~s~n",[html_format(Data)]). + +%%%----------------------------------------------------------------- +%%% Hide password elements from XML data +hide_password(Bin) -> + re:replace(Bin,<<"(<password[^>]*>)[^<]*(</password>)">>,<<"\\1*****\\2">>, + [global,{return,binary}]). + +%%%----------------------------------------------------------------- +%%% HTML formatting +html_format(Bin) -> + binary:replace(indent(Bin),<<"<">>,<<"<">>,[global]). + +%%%----------------------------------------------------------------- +%%% Indentation of XML code +indent(Bin) -> + String = normalize(hide_password(Bin)), + IndentedString = + case erase(part_of_line) of + undefined -> + indent1(String,[]); + Part -> + indent1(lists:reverse(Part)++String,erase(indent)) + end, + list_to_binary(IndentedString). + +%% Normalizes the XML document by removing all space and newline +%% between two XML tags. +%% Returns a list, no matter if the input was a list or a binary. +normalize(Str) -> + re:replace(Str,<<">[ \r\n\t]+<">>,<<"><">>,[global,{return,list}]). + + +indent1("<?"++Rest1,Indent1) -> + %% Prolog + {Line,Rest2,Indent2} = indent_line(Rest1,Indent1,[$?,$<]), + Line++indent1(Rest2,Indent2); +indent1("</"++Rest1,Indent1) -> + %% Stop tag + {Line,Rest2,Indent2} = indent_line1(Rest1,Indent1,[$/,$<]), + "\n"++Line++indent1(Rest2,Indent2); +indent1("<"++Rest1,Indent1) -> + %% Start- or empty tag + put(tag,get_tag(Rest1)), + {Line,Rest2,Indent2} = indent_line(Rest1,Indent1,[$<]), + "\n"++Line++indent1(Rest2,Indent2); +indent1([H|T],Indent) -> + [H|indent1(T,Indent)]; +indent1([],_Indent) -> + []. + +indent_line("?>"++Rest,Indent,Line) -> + %% Prolog + {lists:reverse(Line)++"?>",Rest,Indent}; +indent_line("/></"++Rest,Indent,Line) -> + %% Empty tag, and stop of parent tag -> one step out in indentation + {Indent++lists:reverse(Line)++"/>","</"++Rest,Indent--" "}; +indent_line("/>"++Rest,Indent,Line) -> + %% Empty tag, then probably next tag -> keep indentation + {Indent++lists:reverse(Line)++"/>",Rest,Indent}; +indent_line("></"++Rest,Indent,Line) -> + LastTag = erase(tag), + case get_tag(Rest) of + LastTag -> + %% Start and stop tag, but no content + indent_line1(Rest,Indent,[$/,$<,$>|Line]); + _ -> + %% Stop tag completed, and then stop tag of parent -> one step out + {Indent++lists:reverse(Line)++">","</"++Rest,Indent--" "} + end; +indent_line("><"++Rest,Indent,Line) -> + %% Stop tag completed, and new tag comming -> keep indentation + {Indent++lists:reverse(Line)++">","<"++Rest," "++Indent}; +indent_line("</"++Rest,Indent,Line) -> + %% Stop tag starting -> search for end of this tag + indent_line1(Rest,Indent,[$/,$<|Line]); +indent_line([H|T],Indent,Line) -> + indent_line(T,Indent,[H|Line]); +indent_line([],Indent,Line) -> + %% The line is not complete - will be continued later + put(part_of_line,Line), + put(indent,Indent), + {[],[],Indent}. + +indent_line1("></"++Rest,Indent,Line) -> + %% Stop tag completed, and then stop tag of parent -> one step out + {Indent++lists:reverse(Line)++">","</"++Rest,Indent--" "}; +indent_line1(">"++Rest,Indent,Line) -> + %% Stop tag completed -> keep indentation + {Indent++lists:reverse(Line)++">",Rest,Indent}; +indent_line1([H|T],Indent,Line) -> + indent_line1(T,Indent,[H|Line]); +indent_line1([],Indent,Line) -> + %% The line is not complete - will be continued later + put(part_of_line,Line), + put(indent,Indent), + {[],[],Indent}. + +get_tag("/>"++_) -> + []; +get_tag(">"++_) -> + []; +get_tag([H|T]) -> + [H|get_tag(T)]; +get_tag([]) -> + %% The line is not complete - will be continued later. + []. + + +%%%----------------------------------------------------------------- +%%% SSH stuff +ssh_receive_data() -> + receive + {ssh_cm, _CM, {data, _Ch, _Type, Data}} -> + {ok, Data}; + {ssh_cm, _CM, {Closed, _Ch}} = X when Closed == closed; Closed == eof -> + {error,X}; + {_Ref,timeout} = X -> + {error,X} + end. + +ssh_open(#options{host=Host,timeout=Timeout,port=Port,ssh=SshOpts,name=Name}) -> + case ssh:connect(Host, Port, + [{user_interaction,false}, + {silently_accept_hosts, true}|SshOpts]) of + {ok,CM} -> + case ssh_connection:session_channel(CM, Timeout) of + {ok,Ch} -> + case ssh_connection:subsystem(CM, Ch, "netconf", Timeout) of + success -> + {ok, #connection{reference = {CM,Ch}, + host = Host, + port = Port, + name = Name}}; + failure -> + ssh:close(CM), + {error,{ssh,could_not_execute_netconf_subsystem}} + end; + {error, Reason} -> + ssh:close(CM), + {error,{ssh,could_not_open_channel,Reason}}; + Other -> + %% Bug in ssh?? got {closed,0} here once... + {error,{ssh,unexpected_from_session_channel,Other}} + end; + {error,Reason} -> + {error,{ssh,could_not_connect_to_server,Reason}} + end. + +ssh_send(#connection{reference = {CM,Ch}}, Data) -> + case ssh_connection:send(CM, Ch, Data) of + ok -> ok; + {error,Reason} -> {error,{ssh,failed_to_send_data,Reason}} + end. + +ssh_close(#connection{reference = {CM,_Ch}}) -> + ssh:close(CM). + + +%%---------------------------------------------------------------------- +%% END OF MODULE +%%---------------------------------------------------------------------- diff --git a/lib/common_test/src/ct_netconfc.hrl b/lib/common_test/src/ct_netconfc.hrl new file mode 100644 index 0000000000..295a61a98b --- /dev/null +++ b/lib/common_test/src/ct_netconfc.hrl @@ -0,0 +1,58 @@ +%%-------------------------------------------------------------------- +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2012. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +%%---------------------------------------------------------------------- +%% File: ct_netconfc.hrl +%% +%% Description: +%% This file defines constant values and records used by the +%% netconf client ct_netconfc. +%% +%% @author Support +%% @doc Netconf Client Interface. +%% @end +%%---------------------------------------------------------------------- +%%---------------------------------------------------------------------- + + +%% Default port number (RFC 4742/IANA). +-define(DEFAULT_PORT, 830). + +%% Default timeout to wait for netconf server to reply to a request +-define(DEFAULT_TIMEOUT, infinity). %% msec + +%% Namespaces +-define(NETCONF_NAMESPACE_ATTR,[{xmlns,?NETCONF_NAMESPACE}]). +-define(ACTION_NAMESPACE_ATTR,[{xmlns,?ACTION_NAMESPACE}]). +-define(NETCONF_NOTIF_NAMESPACE_ATTR,[{xmlns,?NETCONF_NOTIF_NAMESPACE}]). +-define(NETMOD_NOTIF_NAMESPACE_ATTR,[{xmlns,?NETMOD_NOTIF_NAMESPACE}]). + +-define(NETCONF_NAMESPACE,"urn:ietf:params:xml:ns:netconf:base:1.0"). +-define(ACTION_NAMESPACE,"urn:com:ericsson:ecim:1.0"). +-define(NETCONF_NOTIF_NAMESPACE, + "urn:ietf:params:xml:ns:netconf:notification:1.0"). +-define(NETMOD_NOTIF_NAMESPACE,"urn:ietf:params:xml:ns:netmod:notification"). + +%% Capabilities +-define(NETCONF_BASE_CAP,"urn:ietf:params:netconf:base:"). +-define(NETCONF_BASE_CAP_VSN,"1.0"). + +%% Misc +-define(END_TAG,<<"]]>]]>">>). + +-define(FORMAT(_F, _A), lists:flatten(io_lib:format(_F, _A))). diff --git a/lib/common_test/src/ct_repeat.erl b/lib/common_test/src/ct_repeat.erl index 8ecd82f771..a47309c6ee 100644 --- a/lib/common_test/src/ct_repeat.erl +++ b/lib/common_test/src/ct_repeat.erl @@ -41,72 +41,86 @@ loop_test(If,Args) when is_list(Args) -> case get_loop_info(Args) of no_loop -> false; - {error,E} -> + E = {error,_} -> io:format("Common Test error: ~p\n\n",[E]), file:set_cwd(Cwd), E; {repeat,N} -> io:format("\nCommon Test: Will repeat tests ~w times.\n\n",[N]), Args1 = [{loop_info,[{repeat,1,N}]} | Args], - loop(If,repeat,0,N,undefined,Args1,undefined), - file:set_cwd(Cwd); + Result = loop(If,repeat,0,N,undefined,Args1,undefined,[]), + file:set_cwd(Cwd), + Result; {stop_time,StopTime} -> - case remaining_time(StopTime) of - 0 -> - io:format("\nCommon Test: No time left to run tests.\n\n",[]), - ok; - Secs -> - io:format("\nCommon Test: Will repeat tests for ~s.\n\n", - [ts(Secs)]), - TPid = - case lists:keymember(force_stop,1,Args) of - true -> - CtrlPid = self(), - spawn(fun() -> stop_after(CtrlPid,Secs) end); - false -> - undefined - end, - Args1 = [{loop_info,[{stop_time,Secs,StopTime,1}]} | Args], - loop(If,stop_time,0,Secs,StopTime,Args1,TPid) - end, - file:set_cwd(Cwd) + Result = + case remaining_time(StopTime) of + 0 -> + io:format("\nCommon Test: " + "No time left to run tests.\n\n",[]), + {error,not_enough_time}; + Secs -> + io:format("\nCommon Test: " + "Will repeat tests for ~s.\n\n",[ts(Secs)]), + TPid = + case lists:keymember(force_stop,1,Args) of + true -> + CtrlPid = self(), + spawn(fun() -> stop_after(CtrlPid,Secs) end); + false -> + undefined + end, + Args1 = [{loop_info,[{stop_time,Secs,StopTime,1}]} | Args], + loop(If,stop_time,0,Secs,StopTime,Args1,TPid,[]) + end, + file:set_cwd(Cwd), + Result end. -loop(_,repeat,N,N,_,_Args,_) -> - ok; +loop(_,repeat,N,N,_,_Args,_,AccResult) -> + lists:reverse(AccResult); -loop(If,Type,N,Data0,Data1,Args,TPid) -> +loop(If,Type,N,Data0,Data1,Args,TPid,AccResult) -> Pid = spawn_tester(If,self(),Args), receive {'EXIT',Pid,Reason} -> - io:format("Test run crashed! This could be an internal error " - "- please report!\n\n" - "~p\n\n",[Reason]), - cancel(TPid), - {error,Reason}; + case Reason of + {user_error,What} -> + io:format("\nTest run failed!\nReason: ~p\n\n\n", [What]), + cancel(TPid), + {error,What}; + _ -> + io:format("Test run crashed! This could be an internal error " + "- please report!\n\n" + "~p\n\n\n",[Reason]), + cancel(TPid), + {error,Reason} + end; {Pid,{error,Reason}} -> - io:format("\nTest run failed!\nReason: ~p\n\n",[Reason]), + io:format("\nTest run failed!\nReason: ~p\n\n\n",[Reason]), cancel(TPid), {error,Reason}; {Pid,Result} -> if Type == repeat -> - io:format("\nTest run ~w(~w) complete.\n\n",[N+1,Data0]), + io:format("\nTest run ~w(~w) complete.\n\n\n",[N+1,Data0]), lists:keydelete(loop_info,1,Args), Args1 = [{loop_info,[{repeat,N+2,Data0}]} | Args], - loop(If,repeat,N+1,Data0,Data1,Args1,TPid); + loop(If,repeat,N+1,Data0,Data1,Args1,TPid,[Result|AccResult]); Type == stop_time -> case remaining_time(Data1) of 0 -> - io:format("\nTest time (~s) has run out.\n\n",[ts(Data0)]), + io:format("\nTest time (~s) has run out.\n\n\n", + [ts(Data0)]), cancel(TPid), - Result; + lists:reverse([Result|AccResult]); Secs -> io:format("\n~s of test time remaining, " - "starting run #~w...\n\n",[ts(Secs),N+2]), + "starting run #~w...\n\n\n", + [ts(Secs),N+2]), lists:keydelete(loop_info,1,Args), ST = {stop_time,Data0,Data1,N+2}, Args1 = [{loop_info,[ST]} | Args], - loop(If,stop_time,N+1,Data0,Data1,Args1,TPid) + loop(If,stop_time,N+1,Data0,Data1,Args1,TPid, + [Result|AccResult]) end end end. diff --git a/lib/common_test/src/ct_run.erl b/lib/common_test/src/ct_run.erl index 2ca2030723..7c31f88f1f 100644 --- a/lib/common_test/src/ct_run.erl +++ b/lib/common_test/src/ct_run.erl @@ -46,6 +46,10 @@ -define(abs(Name), filename:absname(Name)). -define(testdir(Name, Suite), ct_util:get_testdir(Name, Suite)). +-define(EXIT_STATUS_TEST_SUCCESSFUL, 0). +-define(EXIT_STATUS_TEST_CASE_FAILED, 1). +-define(EXIT_STATUS_TEST_RUN_FAILED, 2). + -define(default_verbosity, [{default,?MAX_VERBOSITY}, {'$unspecified',?MAX_VERBOSITY}]). @@ -58,12 +62,14 @@ step, logdir, logopts = [], + basic_html, verbosity = [], config = [], event_handlers = [], ct_hooks = [], enable_builtin_hooks, include = [], + auto_compile, silent_connections, stylesheet, multiply_timetraps = 1, @@ -107,7 +113,8 @@ script_start() -> end, Flags) end, %% used for purpose of testing the run_test interface - io:format(user, "~n-------------------- START ARGS --------------------~n", []), + io:format(user, "~n-------------------- START ARGS " + "--------------------~n", []), io:format(user, "--- Init args:~n~p~n", [FlagFilter(Init)]), io:format(user, "--- CT args:~n~p~n", [FlagFilter(CtArgs)]), EnvArgs = opts2args(EnvStartOpts), @@ -115,7 +122,8 @@ script_start() -> [EnvStartOpts,EnvArgs]), Merged = merge_arguments(CtArgs ++ EnvArgs), io:format(user, "--- Merged args:~n~p~n", [FlagFilter(Merged)]), - io:format(user, "----------------------------------------------------~n~n", []), + io:format(user, "-----------------------------------" + "-----------------~n~n", []), Merged; _ -> merge_arguments(CtArgs) @@ -127,50 +135,100 @@ script_start() -> script_start(Args) -> Tracing = start_trace(Args), - Res = - case ct_repeat:loop_test(script, Args) of - false -> - {ok,Cwd} = file:get_cwd(), - CTVsn = - case filename:basename(code:lib_dir(common_test)) of - CTBase when is_list(CTBase) -> - case string:tokens(CTBase, "-") of - ["common_test",Vsn] -> " v"++Vsn; - _ -> "" - end - end, - io:format("~nCommon Test~s starting (cwd is ~s)~n~n", - [CTVsn,Cwd]), - Self = self(), - Pid = spawn_link(fun() -> script_start1(Self, Args) end), - receive - {'EXIT',Pid,Reason} -> - case Reason of - {user_error,What} -> - io:format("\nTest run failed!\n" - "Reason: ~p\n\n", [What]), - {error,What}; - _ -> - io:format("Test run crashed! " - "This could be an internal error " - "- please report!\n\n" - "~p\n\n", [Reason]), - {error,Reason} - end; - {Pid,{error,Reason}} -> - io:format("\nTest run failed! Reason:\n~p\n\n", - [Reason]), - {error,Reason}; - {Pid,Result} -> - Result - end; - Result -> - Result - end, + case ct_repeat:loop_test(script, Args) of + false -> + {ok,Cwd} = file:get_cwd(), + CTVsn = + case filename:basename(code:lib_dir(common_test)) of + CTBase when is_list(CTBase) -> + case string:tokens(CTBase, "-") of + ["common_test",Vsn] -> " v"++Vsn; + _ -> "" + end + end, + io:format("~nCommon Test~s starting (cwd is ~s)~n~n", + [CTVsn,Cwd]), + Self = self(), + Pid = spawn_link(fun() -> script_start1(Self, Args) end), + receive + {'EXIT',Pid,Reason} -> + case Reason of + {user_error,What} -> + io:format("\nTest run failed!\nReason: ~p\n\n\n", + [What]), + finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args); + _ -> + io:format("Test run crashed! " + "This could be an internal error " + "- please report!\n\n" + "~p\n\n\n", [Reason]), + finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args) + end; + {Pid,{error,Reason}} -> + io:format("\nTest run failed! Reason:\n~p\n\n\n",[Reason]), + finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args); + {Pid,Result} -> + io:nl(), + finish(Tracing, analyze_test_result(Result, Args), Args) + end; + {error,_LoopReason} -> + finish(Tracing, ?EXIT_STATUS_TEST_RUN_FAILED, Args); + Result -> + io:nl(), + finish(Tracing, analyze_test_result(Result, Args), Args) + end. + +%% analyze the result of one test run, or many (in case of looped test) +analyze_test_result(ok, _) -> + ?EXIT_STATUS_TEST_SUCCESSFUL; +analyze_test_result({error,_Reason}, _) -> + ?EXIT_STATUS_TEST_RUN_FAILED; +analyze_test_result({_Ok,Failed,{_UserSkipped,AutoSkipped}}, Args) -> + if Failed > 0 -> + ?EXIT_STATUS_TEST_CASE_FAILED; + true -> + case AutoSkipped of + 0 -> + ?EXIT_STATUS_TEST_SUCCESSFUL; + _ -> + case get_start_opt(exit_status, + fun([ExitOpt]) -> ExitOpt end, + Args) of + undefined -> + ?EXIT_STATUS_TEST_CASE_FAILED; + "ignore_config" -> + ?EXIT_STATUS_TEST_SUCCESSFUL + end + end + end; +analyze_test_result([Result|Rs], Args) -> + case analyze_test_result(Result, Args) of + ?EXIT_STATUS_TEST_SUCCESSFUL -> + analyze_test_result(Rs, Args); + Other -> + Other + end; +analyze_test_result([], _) -> + ?EXIT_STATUS_TEST_SUCCESSFUL; +analyze_test_result(Unknown, _) -> + io:format("\nTest run failed! Reason:\n~p\n\n\n",[Unknown]), + ?EXIT_STATUS_TEST_RUN_FAILED. + +finish(Tracing, ExitStatus, Args) -> stop_trace(Tracing), timer:sleep(1000), - io:nl(), - Res. + %% it's possible to tell CT to finish execution with a call + %% to a different function than the normal halt/1 BIF + %% (meant to be used mainly for reading the CT exit status) + case get_start_opt(halt_with, + fun([HaltMod,HaltFunc]) -> {list_to_atom(HaltMod), + list_to_atom(HaltFunc)} end, + Args) of + undefined -> + halt(ExitStatus); + {M,F} -> + apply(M, F, [ExitStatus]) + end. script_start1(Parent, Args) -> %% read general start flags @@ -216,7 +274,7 @@ script_start1(Parent, Args) -> end end, %% no_auto_compile + include - IncludeDirs = + {AutoCompile,IncludeDirs} = case proplists:get_value(no_auto_compile, Args) of undefined -> application:set_env(common_test, auto_compile, true), @@ -232,16 +290,16 @@ script_start1(Parent, Args) -> case os:getenv("CT_INCLUDE_PATH") of false -> application:set_env(common_test, include, InclDirs), - InclDirs; + {undefined,InclDirs}; CtInclPath -> AllInclDirs = string:tokens(CtInclPath,[$:,$ ,$,]) ++ InclDirs, application:set_env(common_test, include, AllInclDirs), - AllInclDirs + {undefined,AllInclDirs} end; _ -> application:set_env(common_test, auto_compile, false), - [] + {false,[]} end, %% silent connections SilentConns = @@ -253,20 +311,24 @@ script_start1(Parent, Args) -> Stylesheet = get_start_opt(stylesheet, fun([SS]) -> ?abs(SS) end, Args), %% basic_html - used by ct_logs - case proplists:get_value(basic_html, Args) of - undefined -> - application:set_env(common_test, basic_html, false); - _ -> - application:set_env(common_test, basic_html, true) - end, + BasicHtml = case proplists:get_value(basic_html, Args) of + undefined -> + application:set_env(common_test, basic_html, false), + undefined; + _ -> + application:set_env(common_test, basic_html, true), + true + end, StartOpts = #opts{label = Label, profile = Profile, vts = Vts, shell = Shell, cover = Cover, logdir = LogDir, logopts = LogOpts, + basic_html = BasicHtml, verbosity = Verbosity, event_handlers = EvHandlers, ct_hooks = CTHooks, enable_builtin_hooks = EnableBuiltinHooks, + auto_compile = AutoCompile, include = IncludeDirs, silent_connections = SilentConns, stylesheet = Stylesheet, @@ -366,9 +428,36 @@ script_start2(StartOpts = #opts{vts = undefined, StartOpts#opts.enable_builtin_hooks, SpecStartOpts#opts.enable_builtin_hooks), + Stylesheet = + choose_val(StartOpts#opts.stylesheet, + SpecStartOpts#opts.stylesheet), + AllInclude = merge_vals([StartOpts#opts.include, SpecStartOpts#opts.include]), application:set_env(common_test, include, AllInclude), + + AutoCompile = + case choose_val(StartOpts#opts.auto_compile, + SpecStartOpts#opts.auto_compile) of + undefined -> + true; + ACBool -> + application:set_env(common_test, + auto_compile, + ACBool), + ACBool + end, + + BasicHtml = + case choose_val(StartOpts#opts.basic_html, + SpecStartOpts#opts.basic_html) of + undefined -> + false; + BHBool -> + application:set_env(common_test, basic_html, + BHBool), + BHBool + end, {TS,StartOpts#opts{label = Label, profile = Profile, @@ -376,12 +465,15 @@ script_start2(StartOpts = #opts{vts = undefined, cover = Cover, logdir = LogDir, logopts = AllLogOpts, + basic_html = BasicHtml, verbosity = AllVerbosity, config = SpecStartOpts#opts.config, event_handlers = AllEvHs, ct_hooks = AllCTHooks, enable_builtin_hooks = EnableBuiltinHooks, + stylesheet = Stylesheet, + auto_compile = AutoCompile, include = AllInclude, multiply_timetraps = MultTT, scale_timetraps = ScaleTT, @@ -570,7 +662,7 @@ script_start4(#opts{vts = true, cover = Cover}, _) -> %% Add support later (maybe). io:format("\nCan't run cover in vts mode.\n\n", []) end, - erlang:halt(); + {error,no_cover_in_vts_mode}; script_start4(#opts{shell = true, cover = Cover}, _) -> case Cover of @@ -579,7 +671,8 @@ script_start4(#opts{shell = true, cover = Cover}, _) -> _ -> %% Add support later (maybe). io:format("\nCan't run cover in interactive mode.\n\n", []) - end; + end, + {error,no_cover_in_interactive_mode}; script_start4(Opts = #opts{tests = Tests}, Args) -> do_run(Tests, [], Opts, Args). @@ -722,7 +815,7 @@ run_test(StartOpts) when is_list(StartOpts) -> Ref = monitor(process, CTPid), receive {'DOWN',Ref,process,CTPid,{user_error,Error}} -> - Error; + {error,Error}; {'DOWN',Ref,process,CTPid,Other} -> Other end. @@ -840,7 +933,7 @@ run_test2(StartOpts) -> CreatePrivDir = get_start_opt(create_priv_dir, value, StartOpts), %% auto compile & include files - Include = + {AutoCompile,Include} = case proplists:get_value(auto_compile, StartOpts) of undefined -> application:set_env(common_test, auto_compile, true), @@ -856,16 +949,16 @@ run_test2(StartOpts) -> case os:getenv("CT_INCLUDE_PATH") of false -> application:set_env(common_test, include, InclDirs), - InclDirs; + {undefined,InclDirs}; CtInclPath -> InclDirs1 = string:tokens(CtInclPath, [$:,$ ,$,]), AllInclDirs = InclDirs1++InclDirs, application:set_env(common_test, include, AllInclDirs), - AllInclDirs + {undefined,AllInclDirs} end; ACBool -> application:set_env(common_test, auto_compile, ACBool), - [] + {ACBool,[]} end, %% decrypt config file @@ -879,11 +972,14 @@ run_test2(StartOpts) -> end, %% basic html - used by ct_logs - case proplists:get_value(basic_html, StartOpts) of - undefined -> - application:set_env(common_test, basic_html, false); - BasicHtmlBool -> - application:set_env(common_test, basic_html, BasicHtmlBool) + BasicHtml = + case proplists:get_value(basic_html, StartOpts) of + undefined -> + application:set_env(common_test, basic_html, false), + undefined; + BasicHtmlBool -> + application:set_env(common_test, basic_html, BasicHtmlBool), + BasicHtmlBool end, %% stepped execution @@ -891,11 +987,13 @@ run_test2(StartOpts) -> Opts = #opts{label = Label, profile = Profile, cover = Cover, step = Step, logdir = LogDir, - logopts = LogOpts, config = CfgFiles, + logopts = LogOpts, basic_html = BasicHtml, + config = CfgFiles, verbosity = Verbosity, event_handlers = EvHandlers, ct_hooks = CTHooks, enable_builtin_hooks = EnableBuiltinHooks, + auto_compile = AutoCompile, include = Include, silent_connections = SilentConns, stylesheet = Stylesheet, @@ -930,7 +1028,7 @@ run_spec_file(Relaxed, log_ts_names(AbsSpecs), case catch ct_testspec:collect_tests_from_file(AbsSpecs, Relaxed) of {Error,CTReason} when Error == error ; Error == 'EXIT' -> - exit(CTReason); + exit({error,CTReason}); TS -> SpecOpts = get_data_for_node(TS, node()), Label = choose_val(Opts#opts.label, @@ -941,6 +1039,8 @@ run_spec_file(Relaxed, SpecOpts#opts.logdir), AllLogOpts = merge_vals([Opts#opts.logopts, SpecOpts#opts.logopts]), + Stylesheet = choose_val(Opts#opts.stylesheet, + SpecOpts#opts.stylesheet), AllVerbosity = merge_keyvals([Opts#opts.verbosity, SpecOpts#opts.verbosity]), AllConfig = merge_vals([CfgFiles, SpecOpts#opts.config]), @@ -956,7 +1056,6 @@ run_spec_file(Relaxed, SpecOpts#opts.event_handlers]), AllInclude = merge_vals([Opts#opts.include, SpecOpts#opts.include]), - AllCTHooks = merge_vals([Opts#opts.ct_hooks, SpecOpts#opts.ct_hooks]), EnableBuiltinHooks = choose_val(Opts#opts.enable_builtin_hooks, @@ -964,14 +1063,37 @@ run_spec_file(Relaxed, application:set_env(common_test, include, AllInclude), + AutoCompile = case choose_val(Opts#opts.auto_compile, + SpecOpts#opts.auto_compile) of + undefined -> + true; + ACBool -> + application:set_env(common_test, auto_compile, + ACBool), + ACBool + end, + + BasicHtml = case choose_val(Opts#opts.basic_html, + SpecOpts#opts.basic_html) of + undefined -> + false; + BHBool -> + application:set_env(common_test, basic_html, + BHBool), + BHBool + end, + Opts1 = Opts#opts{label = Label, profile = Profile, cover = Cover, logdir = which(logdir, LogDir), logopts = AllLogOpts, + stylesheet = Stylesheet, + basic_html = BasicHtml, verbosity = AllVerbosity, config = AllConfig, event_handlers = AllEvHs, + auto_compile = AutoCompile, include = AllInclude, testspecs = AbsSpecs, multiply_timetraps = MultTT, @@ -987,7 +1109,7 @@ run_spec_file(Relaxed, {Run,Skip} = ct_testspec:prepare_tests(TS, node()), reformat_result(catch do_run(Run, Skip, Opts1, StartOpts)); {error,GCFReason} -> - exit(GCFReason) + exit({error,GCFReason}) end end. @@ -999,8 +1121,8 @@ run_prepared(Run, Skip, Opts = #opts{logdir = LogDir, ok -> reformat_result(catch do_run(Run, Skip, Opts#opts{logdir = LogDir1}, StartOpts)); - {error,Reason} -> - exit(Reason) + {error,_Reason} = Error -> + exit(Error) end. check_config_file(Callback, File)-> @@ -1008,7 +1130,7 @@ check_config_file(Callback, File)-> false -> case code:load_file(Callback) of {module,_} -> ok; - {error,Why} -> exit({cant_load_callback_module,Why}) + {error,Why} -> exit({error,{cant_load_callback_module,Why}}) end; _ -> ok @@ -1019,16 +1141,17 @@ check_config_file(Callback, File)-> {ok,{config,_}}-> File; {error,{wrong_config,Message}}-> - exit({wrong_config,{Callback,Message}}); + exit({error,{wrong_config,{Callback,Message}}}); {error,{nofile,File}}-> - exit({no_such_file,?abs(File)}) + exit({error,{no_such_file,?abs(File)}}) end. run_dir(Opts = #opts{logdir = LogDir, config = CfgFiles, event_handlers = EvHandlers, ct_hooks = CTHook, - enable_builtin_hooks = EnableBuiltinHooks }, StartOpts) -> + enable_builtin_hooks = EnableBuiltinHooks}, + StartOpts) -> LogDir1 = which(logdir, LogDir), Opts1 = Opts#opts{logdir = LogDir1}, AbsCfgFiles = @@ -1041,7 +1164,8 @@ run_dir(Opts = #opts{logdir = LogDir, {module,Callback}-> ok; {error,_}-> - exit({no_such_module,Callback}) + exit({error,{no_such_module, + Callback}}) end end, {Callback, @@ -1054,7 +1178,7 @@ run_dir(Opts = #opts{logdir = LogDir, {ct_hooks, CTHook}, {enable_builtin_hooks,EnableBuiltinHooks}], LogDir1) of ok -> ok; - {error,IReason} -> exit(IReason) + {error,_IReason} = IError -> exit(IError) end, case {proplists:get_value(dir, StartOpts), proplists:get_value(suite, StartOpts), @@ -1096,7 +1220,7 @@ run_dir(Opts = #opts{logdir = LogDir, [], Opts1, StartOpts)); {undefined,[Hd,_|_],_GsAndCs} when not is_integer(Hd) -> - exit(multiple_suites_and_cases); + exit({error,multiple_suites_and_cases}); {undefined,Suite=[Hd|Tl],GsAndCs} when is_integer(Hd) ; (is_list(Hd) and (Tl == [])) ; @@ -1106,10 +1230,10 @@ run_dir(Opts = #opts{logdir = LogDir, [], Opts1, StartOpts)); {[Hd,_|_],_Suites,[]} when is_list(Hd) ; not is_integer(Hd) -> - exit(multiple_dirs_and_suites); + exit({error,multiple_dirs_and_suites}); {undefined,undefined,GsAndCs} when GsAndCs /= [] -> - exit(incorrect_start_options); + exit({error,incorrect_start_options}); {Dir,Suite,GsAndCs} when is_integer(hd(Dir)) ; (is_atom(Dir) and (Dir /= undefined)) ; @@ -1118,7 +1242,7 @@ run_dir(Opts = #opts{logdir = LogDir, Dir1 = if is_atom(Dir) -> atom_to_list(Dir); true -> Dir end, if Suite == undefined -> - exit(incorrect_start_options); + exit({error,incorrect_start_options}); is_integer(hd(Suite)) ; (is_atom(Suite) and (Suite /= undefined)) ; @@ -1137,7 +1261,7 @@ run_dir(Opts = #opts{logdir = LogDir, is_list(Suite) -> % multiple suites case [suite_to_test(Dir1, S) || S <- Suite] of [_,_|_] when GsAndCs /= [] -> - exit(multiple_suites_and_cases); + exit({error,multiple_suites_and_cases}); [{Dir2,Mod}] when GsAndCs /= [] -> reformat_result(catch do_run(tests(Dir2, Mod, GsAndCs), [], Opts1, StartOpts)); @@ -1148,10 +1272,10 @@ run_dir(Opts = #opts{logdir = LogDir, end; {undefined,undefined,[]} -> - exit(no_test_specified); + exit({error,no_test_specified}); {Dir,Suite,GsAndCs} -> - exit({incorrect_start_options,{Dir,Suite,GsAndCs}}) + exit({error,{incorrect_start_options,{Dir,Suite,GsAndCs}}}) end. %%%----------------------------------------------------------------- @@ -1196,7 +1320,7 @@ run_testspec2(File) when is_list(File), is_integer(hd(File)) -> run_testspec2(TestSpec) -> case catch ct_testspec:collect_tests_from_list(TestSpec, false) of {E,CTReason} when E == error ; E == 'EXIT' -> - exit(CTReason); + exit({error,CTReason}); TS -> Opts = get_data_for_node(TS, node()), @@ -1218,8 +1342,8 @@ run_testspec2(TestSpec) -> include = AllInclude}, {Run,Skip} = ct_testspec:prepare_tests(TS, node()), reformat_result(catch do_run(Run, Skip, Opts1, [])); - {error,GCFReason} -> - exit(GCFReason) + {error,_GCFReason} = GCFError -> + exit(GCFError) end end. @@ -1227,6 +1351,8 @@ get_data_for_node(#testspec{label = Labels, profile = Profiles, logdir = LogDirs, logopts = LogOptsList, + basic_html = BHs, + stylesheet = SSs, verbosity = VLvls, cover = CoverFs, config = Cfgs, @@ -1234,6 +1360,7 @@ get_data_for_node(#testspec{label = Labels, event_handler = EvHs, ct_hooks = CTHooks, enable_builtin_hooks = EnableBuiltinHooks, + auto_compile = ACs, include = Incl, multiply_timetraps = MTs, scale_timetraps = STs, @@ -1248,6 +1375,8 @@ get_data_for_node(#testspec{label = Labels, undefined -> []; LOs -> LOs end, + BasicHtml = proplists:get_value(Node, BHs), + Stylesheet = proplists:get_value(Node, SSs), Verbosity = case proplists:get_value(Node, VLvls) of undefined -> []; Lvls -> Lvls @@ -1260,17 +1389,21 @@ get_data_for_node(#testspec{label = Labels, [CBF || {N,CBF} <- UsrCfgs, N==Node], EvHandlers = [{H,A} || {N,H,A} <- EvHs, N==Node], FiltCTHooks = [Hook || {N,Hook} <- CTHooks, N==Node], + AutoCompile = proplists:get_value(Node, ACs), Include = [I || {N,I} <- Incl, N==Node], #opts{label = Label, profile = Profile, logdir = LogDir, logopts = LogOpts, + basic_html = BasicHtml, + stylesheet = Stylesheet, verbosity = Verbosity, cover = Cover, config = ConfigFiles, event_handlers = EvHandlers, ct_hooks = FiltCTHooks, enable_builtin_hooks = EnableBuiltinHooks, + auto_compile = AutoCompile, include = Include, multiply_timetraps = MT, scale_timetraps = ST, @@ -1451,7 +1584,7 @@ do_run(Tests, Skip, Opts, Args) when is_record(Opts, opts) -> case code:which(test_server) of non_existing -> - exit({error,no_path_to_test_server}); + {error,no_path_to_test_server}; _ -> Opts1 = if Cover == undefined -> Opts; @@ -1532,7 +1665,7 @@ do_run(Tests, Skip, Opts, Args) when is_record(Opts, opts) -> exit(Error); _ -> ct_util:stop(normal), - R + TestResult end; false -> io:nl(), @@ -1608,9 +1741,10 @@ verify_suites(TestSuites) -> if Suite == all -> {[DS|Found],NotFound}; true -> - Beam = - filename:join(TestDir, - atom_to_list(Suite)++".beam"), + Beam = filename:join(TestDir, + atom_to_list(Suite)++ + ".beam"), + case filelib:is_regular(Beam) of true -> {[DS|Found],NotFound}; @@ -1619,9 +1753,10 @@ verify_suites(TestSuites) -> {file,SuiteFile} -> %% test suite is already %% loaded and since - %% auto_compile == false, let's - %% assume the user has loaded - %% the beam file explicitly + %% auto_compile == false, + %% let's assume the user has + %% loaded the beam file + %% explicitly ActualDir = filename:dirname(SuiteFile), {[{ActualDir,Suite}|Found], @@ -1629,7 +1764,8 @@ verify_suites(TestSuites) -> false -> Name = filename:join(TestDir, - atom_to_list(Suite)), + atom_to_list( + Suite)), io:format(user, "Suite ~w not found" "in directory ~s~n", @@ -1686,7 +1822,7 @@ step(TestDir, Suite, Case) -> %%% @equiv ct:step/4 step(TestDir, Suite, Case, Opts) when is_list(TestDir), is_atom(Suite), is_atom(Case), - Suite =/= all, Case =/= all -> + Suite =/= all, Case =/= all -> do_run([{TestDir,Suite,Case}], [{step,Opts}]). @@ -1887,8 +2023,8 @@ do_run_test(Tests, Skip, Opts) -> incl_mods = CovIncl, cross = CovCross, src = _CovSrc}} -> - ct_logs:log("COVER INFO","Using cover " - "specification file: ~s~n" + ct_logs:log("COVER INFO", + "Using cover specification file: ~s~n" "App: ~w~n" "Cross cover: ~w~n" "Including ~w modules~n" @@ -1902,8 +2038,8 @@ do_run_test(Tests, Skip, Opts) -> true -> DelResult = file:delete(CovExport), ct_logs:log("COVER INFO", - "Warning! Export file ~s " - "already exists. " + "Warning! " + "Export file ~s already exists. " "Deleting with result: ~p", [CovExport,DelResult]); false -> @@ -1984,7 +2120,13 @@ do_run_test(Tests, Skip, Opts) -> maybe_cleanup_interpret(Suite, Opts#opts.step) end, CleanUp), [code:del_path(Dir) || Dir <- AddedToPath], - ok; + + case ct_util:get_testdata(stats) of + Stats = {_Ok,_Failed,{_UserSkipped,_AutoSkipped}} -> + Stats; + _ -> + {error,test_result_unknown} + end; Error -> Error end. @@ -2567,7 +2709,11 @@ make_abs1([], Path) -> %% to ct_run start arguments (on the init arguments format) - %% this is useful mainly for testing the ct_run start functions. opts2args(EnvStartOpts) -> - lists:flatmap(fun({config,CfgFiles}) -> + lists:flatmap(fun({exit_status,ExitStatusOpt}) when is_atom(ExitStatusOpt) -> + [{exit_status,[atom_to_list(ExitStatusOpt)]}]; + ({halt_with,{HaltM,HaltF}}) -> + [{halt_with,[atom_to_list(HaltM),atom_to_list(HaltF)]}]; + ({config,CfgFiles}) -> [{ct_config,[CfgFiles]}]; ({userconfig,{CBM,CfgStr=[X|_]}}) when is_integer(X) -> [{userconfig,[atom_to_list(CBM),CfgStr]}]; @@ -2617,7 +2763,7 @@ opts2args(EnvStartOpts) -> ({decrypt,{file,File}}) -> [{ct_decrypt_file,[File]}]; ({basic_html,true}) -> - ({basic_html,[]}); + [{basic_html,[]}]; ({basic_html,false}) -> []; ({event_handler,EH}) when is_atom(EH) -> diff --git a/lib/common_test/src/ct_testspec.erl b/lib/common_test/src/ct_testspec.erl index 6d7db7bb75..d66caef100 100644 --- a/lib/common_test/src/ct_testspec.erl +++ b/lib/common_test/src/ct_testspec.erl @@ -29,6 +29,8 @@ -include("ct_util.hrl"). +-define(testspec_fields, record_info(fields, testspec)). + %%%------------------------------------------------------------------ %%% NOTE: %%% Multiple testspecs may be used as input with the result that @@ -46,7 +48,8 @@ %%% Version 1 - extract and return all tests and skips for Node %%% (incl all_nodes) %%%------------------------------------------------------------------- -prepare_tests(TestSpec,Node) when is_record(TestSpec,testspec), is_atom(Node) -> +prepare_tests(TestSpec,Node) when is_record(TestSpec,testspec), + is_atom(Node) -> case lists:keysearch(Node,1,prepare_tests(TestSpec)) of {value,{Node,Run,Skip}} -> {Run,Skip}; @@ -249,22 +252,23 @@ collect_tests_from_file1([Spec|Specs],TestSpec,Relaxed) -> SpecDir = filename:dirname(filename:absname(Spec)), case file:consult(Spec) of {ok,Terms} -> - TestSpec1 = collect_tests(Terms, - TestSpec#testspec{spec_dir=SpecDir}, - Relaxed), - collect_tests_from_file1(Specs,TestSpec1,Relaxed); + case collect_tests(Terms, + TestSpec#testspec{spec_dir=SpecDir}, + Relaxed) of + TS = #testspec{tests=Tests, logdir=LogDirs} when Specs == [] -> + LogDirs1 = lists:delete(".",LogDirs) ++ ["."], + TS#testspec{tests=lists:flatten(Tests), logdir=LogDirs1}; + TS = #testspec{alias = As, nodes = Ns} -> + TS1 = TS#testspec{alias = lists:reverse(As), + nodes = lists:reverse(Ns)}, + collect_tests_from_file1(Specs,TS1,Relaxed) + end; {error,Reason} -> ReasonStr = lists:flatten(io_lib:format("~s", [file:format_error(Reason)])), throw({error,{Spec,ReasonStr}}) - end; -collect_tests_from_file1([],TS=#testspec{config=Cfgs,event_handler=EvHs, - include=Incl,tests=Tests},_) -> - TS#testspec{config=lists:reverse(Cfgs), - event_handler=lists:reverse(EvHs), - include=lists:reverse(Incl), - tests=lists:flatten(Tests)}. + end. collect_tests_from_list(Terms,Relaxed) -> collect_tests_from_list(Terms,[node()],Relaxed). @@ -278,30 +282,163 @@ collect_tests_from_list(Terms,Nodes,Relaxed) when is_list(Nodes) -> E = {error,_} -> E; TS -> - #testspec{config=Cfgs,event_handler=EvHs,include=Incl,tests=Tests} = TS, - TS#testspec{config=lists:reverse(Cfgs), - event_handler=lists:reverse(EvHs), - include=lists:reverse(Incl), - tests=lists:flatten(Tests)} + #testspec{tests=Tests, logdir=LogDirs} = TS, + LogDirs1 = lists:delete(".",LogDirs) ++ ["."], + TS#testspec{tests=lists:flatten(Tests), logdir=LogDirs1} end. collect_tests(Terms,TestSpec,Relaxed) -> put(relaxed,Relaxed), - TestSpec1 = get_global(Terms,TestSpec), - TestSpec2 = get_all_nodes(Terms,TestSpec1), - {Terms2, TestSpec3} = filter_init_terms(Terms, [], TestSpec2), + Terms1 = replace_names(Terms), + TestSpec1 = get_global(Terms1,TestSpec), + TestSpec2 = get_all_nodes(Terms1,TestSpec1), + {Terms2, TestSpec3} = filter_init_terms(Terms1, [], TestSpec2), add_tests(Terms2,TestSpec3). -get_global([{merge_tests, Bool} | Ts], Spec) -> - get_global(Ts,Spec#testspec{ merge_tests = Bool }); +%% replace names (atoms) in the testspec matching those in 'define' terms by +%% searching recursively through tuples and lists +replace_names(Terms) -> + Defs = + lists:flatmap(fun(Def={define,Name,_Replacement}) -> + %% check that name follows convention + if not is_atom(Name) -> + throw({illegal_name_in_testspec,Name}); + true -> + [First|_] = atom_to_list(Name), + if ((First == $?) or (First == $$) + or (First == $_) + or ((First >= $A) + and (First =< $Z))) -> + [Def]; + true -> + throw({illegal_name_in_testspec, + Name}) + end + end; + (_) -> [] + end, Terms), + DefProps = replace_names_in_defs(Defs,[]), + replace_names(Terms,[],DefProps). + +replace_names_in_defs([Def|Left],ModDefs) -> + [{define,Name,Replacement}] = replace_names([Def],[],ModDefs), + replace_names_in_defs(Left,[{Name,Replacement}|ModDefs]); +replace_names_in_defs([],ModDefs) -> + ModDefs. + +replace_names([Term|Ts],Modified,Defs) when is_tuple(Term) -> + [TypeTag|Data] = tuple_to_list(Term), + Term1 = list_to_tuple([TypeTag|replace_names_in_elems(Data,[],Defs)]), + replace_names(Ts,[Term1|Modified],Defs); +replace_names([Term|Ts],Modified,Defs) when is_atom(Term) -> + case proplists:get_value(Term,Defs) of + undefined -> + replace_names(Ts,[Term|Modified],Defs); + Replacement -> + replace_names(Ts,[Replacement|Modified],Defs) + end; +replace_names([Term=[Ch|_]|Ts],Modified,Defs) when is_integer(Ch) -> + %% Term *could* be a string, attempt to search through it + Term1 = replace_names_in_string(Term,Defs), + replace_names(Ts,[Term1|Modified],Defs); +replace_names([Term|Ts],Modified,Defs) -> + replace_names(Ts,[Term|Modified],Defs); +replace_names([],Modified,_Defs) -> + lists:reverse(Modified). + +replace_names_in_elems([Elem|Es],Modified,Defs) when is_tuple(Elem) -> + Elem1 = list_to_tuple(replace_names_in_elems(tuple_to_list(Elem),[],Defs)), + replace_names_in_elems(Es,[Elem1|Modified],Defs); +replace_names_in_elems([Elem|Es],Modified,Defs) when is_atom(Elem) -> + case proplists:get_value(Elem,Defs) of + undefined -> + %% if Term is a node name, check it for replacements as well + Elem1 = replace_names_in_node(Elem,Defs), + replace_names_in_elems(Es,[Elem1|Modified],Defs); + Replacement -> + replace_names_in_elems(Es,[Replacement|Modified],Defs) + end; +replace_names_in_elems([Elem=[Ch|_]|Es],Modified,Defs) when is_integer(Ch) -> + %% Term *could* be a string, attempt to search through it + case replace_names_in_string(Elem,Defs) of + Elem -> + List = replace_names_in_elems(Elem,[],Defs), + replace_names_in_elems(Es,[List|Modified],Defs); + Elem1 -> + replace_names_in_elems(Es,[Elem1|Modified],Defs) + end; +replace_names_in_elems([Elem|Es],Modified,Defs) when is_list(Elem) -> + List = replace_names_in_elems(Elem,[],Defs), + replace_names_in_elems(Es,[List|Modified],Defs); +replace_names_in_elems([Elem|Es],Modified,Defs) -> + replace_names_in_elems(Es,[Elem|Modified],Defs); +replace_names_in_elems([],Modified,_Defs) -> + lists:reverse(Modified). + +replace_names_in_string(Term,Defs=[{Name,Replacement=[Ch|_]}|Ds]) + when is_integer(Ch) -> + try re:replace(Term,[$'|atom_to_list(Name)]++"'", + Replacement,[{return,list}]) of + Term -> % no match, proceed + replace_names_in_string(Term,Ds); + Term1 -> + replace_names_in_string(Term1,Defs) + catch + _:_ -> Term % Term is not a string + end; +replace_names_in_string(Term,[_|Ds]) -> + replace_names_in_string(Term,Ds); +replace_names_in_string(Term,[]) -> + Term. + +replace_names_in_node(Node,Defs) -> + String = atom_to_list(Node), + case lists:member($@,String) of + true -> + list_to_atom(replace_names_in_node1(String,Defs)); + false -> + Node + end. + +replace_names_in_node1(NodeStr,Defs=[{Name,Replacement}|Ds]) -> + ReplStr = case Replacement of + [Ch|_] when is_integer(Ch) -> Replacement; + _ when is_atom(Replacement) -> atom_to_list(Replacement); + _ -> false + end, + if ReplStr == false -> + replace_names_in_node1(NodeStr,Ds); + true -> + case re:replace(NodeStr,atom_to_list(Name), + ReplStr,[{return,list}]) of + NodeStr -> % no match, proceed + replace_names_in_node1(NodeStr,Ds); + NodeStr1 -> + replace_names_in_node1(NodeStr1,Defs) + end + end; +replace_names_in_node1(NodeStr,[]) -> + NodeStr. + + +%% global terms that will be used for analysing all other terms in the spec +get_global([{merge_tests,Bool} | Ts], Spec) -> + get_global(Ts,Spec#testspec{merge_tests=Bool}); + +%% the 'define' term replaces the 'alias' and 'node' terms, but we need to keep +%% the latter two for backwards compatibility... get_global([{alias,Ref,Dir}|Ts],Spec=#testspec{alias=Refs}) -> get_global(Ts,Spec#testspec{alias=[{Ref,get_absdir(Dir,Spec)}|Refs]}); get_global([{node,Ref,Node}|Ts],Spec=#testspec{nodes=Refs}) -> - get_global(Ts,Spec#testspec{nodes=[{Ref,Node}|lists:keydelete(Node,2,Refs)]}); -get_global([_|Ts],Spec) -> get_global(Ts,Spec); -get_global([],Spec) -> Spec. + get_global(Ts,Spec#testspec{nodes=[{Ref,Node} | + lists:keydelete(Node,2,Refs)]}); -get_absfile(Callback, FullName,#testspec{spec_dir=SpecDir}) -> +get_global([_|Ts],Spec) -> + get_global(Ts,Spec); +get_global([],Spec=#testspec{nodes=Ns, alias=As}) -> + Spec#testspec{nodes=lists:reverse(Ns), alias=lists:reverse(As)}. + +get_absfile(Callback,FullName,#testspec{spec_dir=SpecDir}) -> % we need to temporary switch to new cwd here, because % otherwise config files cannot be found {ok, OldWd} = file:get_cwd(), @@ -329,29 +466,45 @@ get_absfile(FullName,#testspec{spec_dir=SpecDir}) -> get_absdir(Dir,#testspec{spec_dir=SpecDir}) -> get_absname(Dir,SpecDir). -get_absname(TestDir,SpecDir) -> - AbsName = filename:absname(TestDir,SpecDir), - TestDirName = filename:basename(AbsName), - Path = filename:dirname(AbsName), - TopDir = filename:basename(Path), - Path1 = - case TopDir of - "." -> - [_|Rev] = lists:reverse(filename:split(Path)), - filename:join(lists:reverse(Rev)); - ".." -> - [_,_|Rev] = lists:reverse(filename:split(Path)), - filename:join(lists:reverse(Rev)); - _ -> - Path - end, - filename:join(Path1,TestDirName). +get_absname(Dir,SpecDir) -> + AbsName = filename:absname(Dir,SpecDir), + shorten_path(AbsName,SpecDir). + +shorten_path(Path,SpecDir) -> + case shorten_split_path(filename:split(Path),[]) of + [] -> + [Root|_] = filename:split(SpecDir), + Root; + Short -> + filename:join(Short) + end. + +shorten_split_path([".."|Path],SoFar) -> + shorten_split_path(Path,tl(SoFar)); +shorten_split_path(["."|Path],SoFar) -> + shorten_split_path(Path,SoFar); +shorten_split_path([Dir|Path],SoFar) -> + shorten_split_path(Path,[Dir|SoFar]); +shorten_split_path([],SoFar) -> + lists:reverse(SoFar). %% go through all tests and register all nodes found get_all_nodes([{suites,Nodes,_,_}|Ts],Spec) when is_list(Nodes) -> get_all_nodes(Ts,save_nodes(Nodes,Spec)); get_all_nodes([{suites,Node,_,_}|Ts],Spec) -> get_all_nodes(Ts,save_nodes([Node],Spec)); +get_all_nodes([{groups,[Char|_],_,_,_}|Ts],Spec) when is_integer(Char) -> + get_all_nodes(Ts,Spec); +get_all_nodes([{groups,Nodes,_,_,_}|Ts],Spec) when is_list(Nodes) -> + get_all_nodes(Ts,save_nodes(Nodes,Spec)); +get_all_nodes([{groups,Nodes,_,_,_,_}|Ts],Spec) when is_list(Nodes) -> + get_all_nodes(Ts,save_nodes(Nodes,Spec)); +get_all_nodes([{groups,_,_,_,{cases,_}}|Ts],Spec) -> + get_all_nodes(Ts,Spec); +get_all_nodes([{groups,Node,_,_,_}|Ts],Spec) -> + get_all_nodes(Ts,save_nodes([Node],Spec)); +get_all_nodes([{groups,Node,_,_,_,_}|Ts],Spec) -> + get_all_nodes(Ts,save_nodes([Node],Spec)); get_all_nodes([{cases,Nodes,_,_,_}|Ts],Spec) when is_list(Nodes) -> get_all_nodes(Ts,save_nodes(Nodes,Spec)); get_all_nodes([{cases,Node,_,_,_}|Ts],Spec) -> @@ -360,74 +513,93 @@ get_all_nodes([{skip_suites,Nodes,_,_,_}|Ts],Spec) when is_list(Nodes) -> get_all_nodes(Ts,save_nodes(Nodes,Spec)); get_all_nodes([{skip_suites,Node,_,_,_}|Ts],Spec) -> get_all_nodes(Ts,save_nodes([Node],Spec)); +get_all_nodes([{skip_groups,[Char|_],_,_,_,_}|Ts],Spec) when is_integer(Char) -> + get_all_nodes(Ts,Spec); +get_all_nodes([{skip_groups,Nodes,_,_,_,_}|Ts],Spec) when is_list(Nodes) -> + get_all_nodes(Ts,save_nodes(Nodes,Spec)); +get_all_nodes([{skip_groups,Node,_,_,_,_}|Ts],Spec) -> + get_all_nodes(Ts,save_nodes([Node],Spec)); +get_all_nodes([{skip_groups,Nodes,_,_,_,_,_}|Ts],Spec) when is_list(Nodes) -> + get_all_nodes(Ts,save_nodes(Nodes,Spec)); +get_all_nodes([{skip_groups,Node,_,_,_,_,_}|Ts],Spec) -> + get_all_nodes(Ts,save_nodes([Node],Spec)); get_all_nodes([{skip_cases,Nodes,_,_,_,_}|Ts],Spec) when is_list(Nodes) -> get_all_nodes(Ts,save_nodes(Nodes,Spec)); get_all_nodes([{skip_cases,Node,_,_,_,_}|Ts],Spec) -> get_all_nodes(Ts,save_nodes([Node],Spec)); -get_all_nodes([_|Ts],Spec) -> +get_all_nodes([_Other|Ts],Spec) -> get_all_nodes(Ts,Spec); get_all_nodes([],Spec) -> Spec. -filter_init_terms([{init, InitOptions}|Ts], NewTerms, Spec)-> - filter_init_terms([{init, list_nodes(Spec), InitOptions}|Ts], NewTerms, Spec); -filter_init_terms([{init, NodeRef, InitOptions}|Ts], NewTerms, Spec) - when is_atom(NodeRef)-> - filter_init_terms([{init, [NodeRef], InitOptions}|Ts], NewTerms, Spec); -filter_init_terms([{init, NodeRefs, InitOption}|Ts], NewTerms, Spec) when is_tuple(InitOption) -> - filter_init_terms([{init, NodeRefs, [InitOption]}|Ts], NewTerms, Spec); -filter_init_terms([{init, [NodeRef|NodeRefs], InitOptions}|Ts], NewTerms, Spec=#testspec{init=InitData})-> - NodeStartOptions = case lists:keyfind(node_start, 1, InitOptions) of - {node_start, NSOptions}-> - case lists:keyfind(callback_module, 1, NSOptions) of - {callback_module, _Callback}-> - NSOptions; - false-> - [{callback_module, ct_slave}|NSOptions] - end; - false-> - [] - end, - EvalTerms = case lists:keyfind(eval, 1, InitOptions) of - {eval, MFA} when is_tuple(MFA)-> - [MFA]; - {eval, MFAs} when is_list(MFAs)-> - MFAs; - false-> - [] - end, +filter_init_terms([{init,InitOptions}|Ts],NewTerms,Spec) -> + filter_init_terms([{init,list_nodes(Spec),InitOptions}|Ts], + NewTerms,Spec); +filter_init_terms([{init,all_nodes,InitOptions}|Ts],NewTerms,Spec) -> + filter_init_terms([{init,list_nodes(Spec),InitOptions}|Ts], + NewTerms,Spec); +filter_init_terms([{init,NodeRef,InitOptions}|Ts], + NewTerms,Spec) when is_atom(NodeRef) -> + filter_init_terms([{init,[NodeRef],InitOptions}|Ts],NewTerms,Spec); +filter_init_terms([{init,NodeRefs,InitOption}|Ts], + NewTerms,Spec) when is_tuple(InitOption) -> + filter_init_terms([{init,NodeRefs,[InitOption]}|Ts],NewTerms,Spec); +filter_init_terms([{init,[NodeRef|NodeRefs],InitOptions}|Ts], + NewTerms,Spec=#testspec{init=InitData}) -> + NodeStartOptions = + case lists:keyfind(node_start,1,InitOptions) of + {node_start,NSOptions}-> + case lists:keyfind(callback_module,1,NSOptions) of + {callback_module,_Callback}-> + NSOptions; + false-> + [{callback_module,ct_slave}|NSOptions] + end; + false-> + [] + end, + EvalTerms = case lists:keyfind(eval,1,InitOptions) of + {eval,MFA} when is_tuple(MFA) -> + [MFA]; + {eval,MFAs} when is_list(MFAs) -> + MFAs; + false-> + [] + end, Node = ref2node(NodeRef,Spec#testspec.nodes), - InitData2 = add_option({node_start, NodeStartOptions}, Node, InitData, true), - InitData3 = add_option({eval, EvalTerms}, Node, InitData2, false), - filter_init_terms([{init, NodeRefs, InitOptions}|Ts], NewTerms, Spec#testspec{init=InitData3}); -filter_init_terms([{init, [], _}|Ts], NewTerms, Spec)-> - filter_init_terms(Ts, NewTerms, Spec); -filter_init_terms([Term|Ts], NewTerms, Spec)-> - filter_init_terms(Ts, [Term|NewTerms], Spec); -filter_init_terms([], NewTerms, Spec)-> - {lists:reverse(NewTerms), Spec}. - -add_option({Key, Value}, Node, List, WarnIfExists) when is_list(Value)-> - OldOptions = case lists:keyfind(Node, 1, List) of - {Node, Options}-> + InitData2 = add_option({node_start,NodeStartOptions},Node,InitData,true), + InitData3 = add_option({eval,EvalTerms},Node,InitData2,false), + filter_init_terms([{init,NodeRefs,InitOptions}|Ts], + NewTerms,Spec#testspec{init=InitData3}); +filter_init_terms([{init,[],_}|Ts],NewTerms,Spec) -> + filter_init_terms(Ts,NewTerms,Spec); +filter_init_terms([Term|Ts],NewTerms,Spec) -> + filter_init_terms(Ts,[Term|NewTerms],Spec); +filter_init_terms([],NewTerms,Spec) -> + {lists:reverse(NewTerms),Spec}. + +add_option({Key,Value},Node,List,WarnIfExists) when is_list(Value) -> + OldOptions = case lists:keyfind(Node,1,List) of + {Node,Options}-> Options; false-> [] end, - NewOption = case lists:keyfind(Key, 1, OldOptions) of - {Key, OldOption} when WarnIfExists, OldOption/=[]-> - io:format("There is an option ~w=~w already defined for node ~p, skipping new ~w~n", - [Key, OldOption, Node, Value]), + NewOption = case lists:keyfind(Key,1,OldOptions) of + {Key,OldOption} when WarnIfExists,OldOption/=[]-> + io:format("There is an option ~w=~w already " + "defined for node ~p, skipping new ~w~n", + [Key,OldOption,Node,Value]), OldOption; - {Key, OldOption}-> + {Key,OldOption}-> OldOption ++ Value; false-> Value end, - lists:keystore(Node, 1, List, - {Node, lists:keystore(Key, 1, OldOptions, {Key, NewOption})}); -add_option({Key, Value}, Node, List, WarnIfExists)-> - add_option({Key, [Value]}, Node, List, WarnIfExists). + lists:keystore(Node,1,List, + {Node,lists:keystore(Key,1,OldOptions,{Key,NewOption})}); +add_option({Key,Value},Node,List,WarnIfExists) -> + add_option({Key,[Value]},Node,List,WarnIfExists). save_nodes(Nodes,Spec=#testspec{nodes=NodeRefs}) -> NodeRefs1 = @@ -446,294 +618,18 @@ save_nodes(Nodes,Spec=#testspec{nodes=NodeRefs}) -> end end end,NodeRefs,Nodes), - Spec#testspec{nodes=NodeRefs1}. + Spec#testspec{nodes=NodeRefs1}. list_nodes(#testspec{nodes=NodeRefs}) -> lists:map(fun({_Ref,Node}) -> Node end, NodeRefs). - -%% --------------------------------------------------------- -%% / \ -%% | When adding tests, remember to update valid_terms/0 also! | -%% \ / -%% --------------------------------------------------------- - - -%% Associate a "global" logdir with all nodes -%% except those with specific logdir, e.g: -%% ["/tmp/logdir",{ct1@finwe,"/tmp/logdir2"}] -%% means all nodes should write to /tmp/logdir -%% except ct1@finwe that should use /tmp/logdir2. - -%% --- logdir --- -add_tests([{logdir,all_nodes,Dir}|Ts],Spec) -> - Dirs = Spec#testspec.logdir, - Tests = [{logdir,N,get_absdir(Dir,Spec)} || - N <- list_nodes(Spec), - lists:keymember(ref2node(N,Spec#testspec.nodes), - 1,Dirs) == false], - add_tests(Tests++Ts,Spec); -add_tests([{logdir,Nodes,Dir}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,logdir,[Dir],Ts,Spec#testspec.nodes), - add_tests(Ts1,Spec); -add_tests([{logdir,Node,Dir}|Ts],Spec) -> - Dirs = Spec#testspec.logdir, - Dirs1 = [{ref2node(Node,Spec#testspec.nodes),get_absdir(Dir,Spec)} | - lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,Dirs)], - add_tests(Ts,Spec#testspec{logdir=Dirs1}); -add_tests([{logdir,Dir}|Ts],Spec) -> - add_tests([{logdir,all_nodes,Dir}|Ts],Spec); - -%% --- logopts --- -add_tests([{logopts,all_nodes,Opts}|Ts],Spec) -> - LogOpts = Spec#testspec.logopts, - Tests = [{logopts,N,Opts} || - N <- list_nodes(Spec), - lists:keymember(ref2node(N,Spec#testspec.nodes),1, - LogOpts) == false], - add_tests(Tests++Ts,Spec); -add_tests([{logopts,Nodes,Opts}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,logopts,[Opts],Ts,Spec#testspec.nodes), - add_tests(Ts1,Spec); -add_tests([{logopts,Node,Opts}|Ts],Spec) -> - LogOpts = Spec#testspec.logopts, - LogOpts1 = [{ref2node(Node,Spec#testspec.nodes),Opts} | - lists:keydelete(ref2node(Node,Spec#testspec.nodes), - 1,LogOpts)], - add_tests(Ts,Spec#testspec{logopts=LogOpts1}); -add_tests([{logopts,Opts}|Ts],Spec) -> - add_tests([{logopts,all_nodes,Opts}|Ts],Spec); - -%% --- verbosity --- -add_tests([{verbosity,all_nodes,VLvls}|Ts],Spec) -> - VLvls0 = Spec#testspec.verbosity, - Tests = [{verbosity,N,VLvls} || - N <- list_nodes(Spec), - lists:keymember(ref2node(N,Spec#testspec.nodes),1, - VLvls0) == false], - add_tests(Tests++Ts,Spec); -add_tests([{verbosity,Nodes,VLvls}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,verbosity,[VLvls],Ts,Spec#testspec.nodes), - add_tests(Ts1,Spec); -add_tests([{verbosity,Node,VLvls}|Ts],Spec) -> - VLvls0 = Spec#testspec.verbosity, - VLvls1 = - if is_integer(VLvls) -> - [{'$unspecified',VLvls}]; - is_list(VLvls) -> - lists:map(fun(VLvl = {_Cat,_Lvl}) -> VLvl; - (Lvl) -> {'$unspecified',Lvl} end, VLvls) - end, - Verbosity = [{ref2node(Node,Spec#testspec.nodes),VLvls1} | - lists:keydelete(ref2node(Node,Spec#testspec.nodes), - 1,VLvls0)], - add_tests(Ts,Spec#testspec{verbosity=Verbosity}); -add_tests([{verbosity,VLvls}|Ts],Spec) -> - add_tests([{verbosity,all_nodes,VLvls}|Ts],Spec); - -%% --- label --- -add_tests([{label,all_nodes,Lbl}|Ts],Spec) -> - Labels = Spec#testspec.label, - Tests = [{label,N,Lbl} || N <- list_nodes(Spec), - lists:keymember(ref2node(N,Spec#testspec.nodes), - 1,Labels) == false], - add_tests(Tests++Ts,Spec); -add_tests([{label,Nodes,Lbl}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,label,[Lbl],Ts,Spec#testspec.nodes), - add_tests(Ts1,Spec); -add_tests([{label,Node,Lbl}|Ts],Spec) -> - Labels = Spec#testspec.label, - Labels1 = [{ref2node(Node,Spec#testspec.nodes),Lbl} | - lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,Labels)], - add_tests(Ts,Spec#testspec{label=Labels1}); -add_tests([{label,Lbl}|Ts],Spec) -> - add_tests([{label,all_nodes,Lbl}|Ts],Spec); - -%% --- cover --- -add_tests([{cover,all_nodes,File}|Ts],Spec) -> - Tests = lists:map(fun(N) -> {cover,N,File} end, list_nodes(Spec)), - add_tests(Tests++Ts,Spec); -add_tests([{cover,Nodes,File}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,cover,[File],Ts,Spec#testspec.nodes), - add_tests(Ts1,Spec); -add_tests([{cover,Node,File}|Ts],Spec) -> - CoverFs = Spec#testspec.cover, - CoverFs1 = [{ref2node(Node,Spec#testspec.nodes),get_absfile(File,Spec)} | - lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,CoverFs)], - add_tests(Ts,Spec#testspec{cover=CoverFs1}); -add_tests([{cover,File}|Ts],Spec) -> - add_tests([{cover,all_nodes,File}|Ts],Spec); - -%% --- multiply_timetraps --- -add_tests([{multiply_timetraps,all_nodes,MT}|Ts],Spec) -> - Tests = lists:map(fun(N) -> {multiply_timetraps,N,MT} end, list_nodes(Spec)), - add_tests(Tests++Ts,Spec); -add_tests([{multiply_timetraps,Nodes,MT}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,multiply_timetraps,[MT],Ts,Spec#testspec.nodes), - add_tests(Ts1,Spec); -add_tests([{multiply_timetraps,Node,MT}|Ts],Spec) -> - MTs = Spec#testspec.multiply_timetraps, - MTs1 = [{ref2node(Node,Spec#testspec.nodes),MT} | - lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,MTs)], - add_tests(Ts,Spec#testspec{multiply_timetraps=MTs1}); -add_tests([{multiply_timetraps,MT}|Ts],Spec) -> - add_tests([{multiply_timetraps,all_nodes,MT}|Ts],Spec); - -%% --- scale_timetraps --- -add_tests([{scale_timetraps,all_nodes,ST}|Ts],Spec) -> - Tests = lists:map(fun(N) -> {scale_timetraps,N,ST} end, list_nodes(Spec)), - add_tests(Tests++Ts,Spec); -add_tests([{scale_timetraps,Nodes,ST}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,scale_timetraps,[ST],Ts,Spec#testspec.nodes), - add_tests(Ts1,Spec); -add_tests([{scale_timetraps,Node,ST}|Ts],Spec) -> - STs = Spec#testspec.scale_timetraps, - STs1 = [{ref2node(Node,Spec#testspec.nodes),ST} | - lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,STs)], - add_tests(Ts,Spec#testspec{scale_timetraps=STs1}); -add_tests([{scale_timetraps,ST}|Ts],Spec) -> - add_tests([{scale_timetraps,all_nodes,ST}|Ts],Spec); - -%% --- create_priv_dir --- -add_tests([{create_priv_dir,all_nodes,PD}|Ts],Spec) -> - Tests = lists:map(fun(N) -> {create_priv_dir,N,PD} end, list_nodes(Spec)), - add_tests(Tests++Ts,Spec); -add_tests([{create_priv_dir,Nodes,PD}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,create_priv_dir,[PD],Ts,Spec#testspec.nodes), - add_tests(Ts1,Spec); -add_tests([{create_priv_dir,Node,PD}|Ts],Spec) -> - PDs = Spec#testspec.create_priv_dir, - PDs1 = [{ref2node(Node,Spec#testspec.nodes),PD} | - lists:keydelete(ref2node(Node,Spec#testspec.nodes),1,PDs)], - add_tests(Ts,Spec#testspec{create_priv_dir=PDs1}); -add_tests([{create_priv_dir,PD}|Ts],Spec) -> - add_tests([{create_priv_dir,all_nodes,PD}|Ts],Spec); - -%% --- config --- -add_tests([{config,all_nodes,Files}|Ts],Spec) -> - Tests = lists:map(fun(N) -> {config,N,Files} end, list_nodes(Spec)), - add_tests(Tests++Ts,Spec); -add_tests([{config,Nodes,Files}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,config,[Files],Ts,Spec#testspec.nodes), - add_tests(Ts1,Spec); -add_tests([{config,Node,[F|Fs]}|Ts],Spec) when is_list(F) -> - Cfgs = Spec#testspec.config, - Node1 = ref2node(Node,Spec#testspec.nodes), - add_tests([{config,Node,Fs}|Ts], - Spec#testspec{config=[{Node1,get_absfile(F,Spec)}|Cfgs]}); -add_tests([{config,_Node,[]}|Ts],Spec) -> - add_tests(Ts,Spec); -add_tests([{config,Node,F}|Ts],Spec) -> - add_tests([{config,Node,[F]}|Ts],Spec); -add_tests([{config,Files}|Ts],Spec) -> - add_tests([{config,all_nodes,Files}|Ts],Spec); - - -%% --- userconfig --- -add_tests([{userconfig,all_nodes,CBF}|Ts],Spec) -> - Tests = lists:map(fun(N) -> {userconfig,N,CBF} end, list_nodes(Spec)), - add_tests(Tests++Ts,Spec); -add_tests([{userconfig,Nodes,CBF}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,userconfig,[CBF],Ts,Spec#testspec.nodes), - add_tests(Ts1,Spec); -add_tests([{userconfig,Node,[{Callback, Config}|CBF]}|Ts],Spec) -> - Cfgs = Spec#testspec.userconfig, - Node1 = ref2node(Node,Spec#testspec.nodes), - add_tests([{userconfig,Node,CBF}|Ts], - Spec#testspec{userconfig=[{Node1,{Callback, - get_absfile(Callback, Config ,Spec)}}|Cfgs]}); -add_tests([{userconfig,_Node,[]}|Ts],Spec) -> - add_tests(Ts,Spec); -add_tests([{userconfig,Node,CBF}|Ts],Spec) -> - add_tests([{userconfig,Node,[CBF]}|Ts],Spec); -add_tests([{userconfig,CBF}|Ts],Spec) -> - add_tests([{userconfig,all_nodes,CBF}|Ts],Spec); - -%% --- event_handler --- -add_tests([{event_handler,all_nodes,Hs}|Ts],Spec) -> - Tests = lists:map(fun(N) -> {event_handler,N,Hs,[]} end, list_nodes(Spec)), - add_tests(Tests++Ts,Spec); -add_tests([{event_handler,all_nodes,Hs,Args}|Ts],Spec) when is_list(Args) -> - Tests = lists:map(fun(N) -> {event_handler,N,Hs,Args} end, list_nodes(Spec)), - add_tests(Tests++Ts,Spec); -add_tests([{event_handler,Hs}|Ts],Spec) -> - add_tests([{event_handler,all_nodes,Hs,[]}|Ts],Spec); -add_tests([{event_handler,HsOrNodes,HsOrArgs}|Ts],Spec) -> - case is_noderef(HsOrNodes,Spec#testspec.nodes) of - true -> % HsOrNodes == Nodes, HsOrArgs == Hs - case {HsOrNodes,HsOrArgs} of - {Nodes,Hs} when is_list(Nodes) -> - Ts1 = separate(Nodes,event_handler,[Hs,[]],Ts, - Spec#testspec.nodes), - add_tests(Ts1,Spec); - {_Node,[]} -> - add_tests(Ts,Spec); - {Node,HOrHs} -> - EvHs = Spec#testspec.event_handler, - Node1 = ref2node(Node,Spec#testspec.nodes), - case HOrHs of - [H|Hs] when is_atom(H) -> - add_tests([{event_handler,Node,Hs}|Ts], - Spec#testspec{event_handler=[{Node1,H,[]}|EvHs]}); - H when is_atom(H) -> - add_tests(Ts,Spec#testspec{event_handler=[{Node1,H,[]}|EvHs]}) - end - end; - false -> % HsOrNodes == Hs, HsOrArgs == Args - add_tests([{event_handler,all_nodes,HsOrNodes,HsOrArgs}|Ts],Spec) - end; -add_tests([{event_handler,Nodes,Hs,Args}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,event_handler,[Hs,Args],Ts,Spec#testspec.nodes), - add_tests(Ts1,Spec); -add_tests([{event_handler,Node,[H|Hs],Args}|Ts],Spec) when is_atom(H) -> - EvHs = Spec#testspec.event_handler, - Node1 = ref2node(Node,Spec#testspec.nodes), - add_tests([{event_handler,Node,Hs,Args}|Ts], - Spec#testspec{event_handler=[{Node1,H,Args}|EvHs]}); -add_tests([{event_handler,_Node,[],_Args}|Ts],Spec) -> - add_tests(Ts,Spec); -add_tests([{event_handler,Node,H,Args}|Ts],Spec) when is_atom(H) -> - EvHs = Spec#testspec.event_handler, - Node1 = ref2node(Node,Spec#testspec.nodes), - add_tests(Ts,Spec#testspec{event_handler=[{Node1,H,Args}|EvHs]}); - -%% --- ct_hooks -- -add_tests([{ct_hooks, all_nodes, Hooks} | Ts], Spec) -> - Tests = [{ct_hooks,N,Hooks} || N <- list_nodes(Spec)], - add_tests(Tests ++ Ts, Spec); -add_tests([{ct_hooks, Node, [Hook|Hooks]}|Ts], Spec) -> - SuiteCbs = Spec#testspec.ct_hooks, - Node1 = ref2node(Node,Spec#testspec.nodes), - add_tests([{ct_hooks, Node, Hooks} | Ts], - Spec#testspec{ct_hooks = [{Node1,Hook} | SuiteCbs]}); -add_tests([{ct_hooks, _Node, []}|Ts], Spec) -> - add_tests(Ts, Spec); -add_tests([{ct_hooks, Hooks}|Ts], Spec) -> - add_tests([{ct_hooks, all_nodes, Hooks}|Ts], Spec); - -%% -- enable_builtin_hooks -- -add_tests([{enable_builtin_hooks,Bool}|Ts],Spec) -> - add_tests(Ts, Spec#testspec{ enable_builtin_hooks = Bool }); - -%% --- include --- -add_tests([{include,all_nodes,InclDirs}|Ts],Spec) -> - Tests = lists:map(fun(N) -> {include,N,InclDirs} end, list_nodes(Spec)), - add_tests(Tests++Ts,Spec); -add_tests([{include,Nodes,InclDirs}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,include,[InclDirs],Ts,Spec#testspec.nodes), - add_tests(Ts1,Spec); -add_tests([{include,Node,[D|Ds]}|Ts],Spec) when is_list(D) -> - Dirs = Spec#testspec.include, - Node1 = ref2node(Node,Spec#testspec.nodes), - add_tests([{include,Node,Ds}|Ts], - Spec#testspec{include=[{Node1,get_absdir(D,Spec)}|Dirs]}); -add_tests([{include,_Node,[]}|Ts],Spec) -> - add_tests(Ts,Spec); -add_tests([{include,Node,D}|Ts],Spec) -> - add_tests([{include,Node,[D]}|Ts],Spec); -add_tests([{include,InclDirs}|Ts],Spec) -> - add_tests([{include,all_nodes,InclDirs}|Ts],Spec); +%% ----------------------------------------------------- +%% / \ +%% | When adding test/config terms, remember to update | +%% | valid_terms/0 also! | +%% \ / +%% ----------------------------------------------------- %% --- suites --- add_tests([{suites,all_nodes,Dir,Ss}|Ts],Spec) -> @@ -746,7 +642,7 @@ add_tests([{suites,Nodes,Dir,Ss}|Ts],Spec) when is_list(Nodes) -> add_tests([{suites,Node,Dir,Ss}|Ts],Spec) -> Tests = Spec#testspec.tests, Tests1 = insert_suites(ref2node(Node,Spec#testspec.nodes), - ref2dir(Dir,Spec#testspec.alias), + ref2dir(Dir,Spec), Ss,Tests, Spec#testspec.merge_tests), add_tests(Ts,Spec#testspec{tests=Tests1}); @@ -766,20 +662,22 @@ add_tests([{groups,Dir,Suite,Gs,{cases,TCs}}|Ts],Spec) -> add_tests([{groups,Nodes,Dir,Suite,Gs}|Ts],Spec) when is_list(Nodes) -> Ts1 = separate(Nodes,groups,[Dir,Suite,Gs],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); -add_tests([{groups,Nodes,Dir,Suite,Gs,{cases,TCs}}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,groups,[Dir,Suite,Gs,{cases,TCs}],Ts,Spec#testspec.nodes), +add_tests([{groups,Nodes,Dir,Suite,Gs,{cases,TCs}}|Ts], + Spec) when is_list(Nodes) -> + Ts1 = separate(Nodes,groups,[Dir,Suite,Gs,{cases,TCs}],Ts, + Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{groups,Node,Dir,Suite,Gs}|Ts],Spec) -> Tests = Spec#testspec.tests, Tests1 = insert_groups(ref2node(Node,Spec#testspec.nodes), - ref2dir(Dir,Spec#testspec.alias), + ref2dir(Dir,Spec), Suite,Gs,all,Tests, Spec#testspec.merge_tests), add_tests(Ts,Spec#testspec{tests=Tests1}); add_tests([{groups,Node,Dir,Suite,Gs,{cases,TCs}}|Ts],Spec) -> Tests = Spec#testspec.tests, Tests1 = insert_groups(ref2node(Node,Spec#testspec.nodes), - ref2dir(Dir,Spec#testspec.alias), + ref2dir(Dir,Spec), Suite,Gs,TCs,Tests, Spec#testspec.merge_tests), add_tests(Ts,Spec#testspec{tests=Tests1}); @@ -795,7 +693,7 @@ add_tests([{cases,Nodes,Dir,Suite,Cs}|Ts],Spec) when is_list(Nodes) -> add_tests([{cases,Node,Dir,Suite,Cs}|Ts],Spec) -> Tests = Spec#testspec.tests, Tests1 = insert_cases(ref2node(Node,Spec#testspec.nodes), - ref2dir(Dir,Spec#testspec.alias), + ref2dir(Dir,Spec), Suite,Cs,Tests, Spec#testspec.merge_tests), add_tests(Ts,Spec#testspec{tests=Tests1}); @@ -810,7 +708,7 @@ add_tests([{skip_suites,Nodes,Dir,Ss,Cmt}|Ts],Spec) when is_list(Nodes) -> add_tests([{skip_suites,Node,Dir,Ss,Cmt}|Ts],Spec) -> Tests = Spec#testspec.tests, Tests1 = skip_suites(ref2node(Node,Spec#testspec.nodes), - ref2dir(Dir,Spec#testspec.alias), + ref2dir(Dir,Spec), Ss,Cmt,Tests, Spec#testspec.merge_tests), add_tests(Ts,Spec#testspec{tests=Tests1}); @@ -819,7 +717,8 @@ add_tests([{skip_suites,Node,Dir,Ss,Cmt}|Ts],Spec) -> add_tests([{skip_groups,all_nodes,Dir,Suite,Gs,Cmt}|Ts],Spec) -> add_tests([{skip_groups,list_nodes(Spec),Dir,Suite,Gs,Cmt}|Ts],Spec); add_tests([{skip_groups,all_nodes,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec) -> - add_tests([{skip_groups,list_nodes(Spec),Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec); + add_tests([{skip_groups,list_nodes(Spec),Dir,Suite,Gs,{cases,TCs},Cmt}|Ts], + Spec); add_tests([{skip_groups,Dir,Suite,Gs,Cmt}|Ts],Spec) -> add_tests([{skip_groups,all_nodes,Dir,Suite,Gs,Cmt}|Ts],Spec); add_tests([{skip_groups,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec) -> @@ -827,20 +726,22 @@ add_tests([{skip_groups,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec) -> add_tests([{skip_groups,Nodes,Dir,Suite,Gs,Cmt}|Ts],Spec) when is_list(Nodes) -> Ts1 = separate(Nodes,skip_groups,[Dir,Suite,Gs,Cmt],Ts,Spec#testspec.nodes), add_tests(Ts1,Spec); -add_tests([{skip_groups,Nodes,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec) when is_list(Nodes) -> - Ts1 = separate(Nodes,skip_groups,[Dir,Suite,Gs,{cases,TCs},Cmt],Ts,Spec#testspec.nodes), +add_tests([{skip_groups,Nodes,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts], + Spec) when is_list(Nodes) -> + Ts1 = separate(Nodes,skip_groups,[Dir,Suite,Gs,{cases,TCs},Cmt],Ts, + Spec#testspec.nodes), add_tests(Ts1,Spec); add_tests([{skip_groups,Node,Dir,Suite,Gs,Cmt}|Ts],Spec) -> Tests = Spec#testspec.tests, Tests1 = skip_groups(ref2node(Node,Spec#testspec.nodes), - ref2dir(Dir,Spec#testspec.alias), + ref2dir(Dir,Spec), Suite,Gs,all,Cmt,Tests, Spec#testspec.merge_tests), add_tests(Ts,Spec#testspec{tests=Tests1}); add_tests([{skip_groups,Node,Dir,Suite,Gs,{cases,TCs},Cmt}|Ts],Spec) -> Tests = Spec#testspec.tests, Tests1 = skip_groups(ref2node(Node,Spec#testspec.nodes), - ref2dir(Dir,Spec#testspec.alias), + ref2dir(Dir,Spec), Suite,Gs,TCs,Cmt,Tests, Spec#testspec.merge_tests), add_tests(Ts,Spec#testspec{tests=Tests1}); @@ -856,45 +757,101 @@ add_tests([{skip_cases,Nodes,Dir,Suite,Cs,Cmt}|Ts],Spec) when is_list(Nodes) -> add_tests([{skip_cases,Node,Dir,Suite,Cs,Cmt}|Ts],Spec) -> Tests = Spec#testspec.tests, Tests1 = skip_cases(ref2node(Node,Spec#testspec.nodes), - ref2dir(Dir,Spec#testspec.alias), + ref2dir(Dir,Spec), Suite,Cs,Cmt,Tests,Spec#testspec.merge_tests), add_tests(Ts,Spec#testspec{tests=Tests1}); +%% --- various configuration terms --- +add_tests([{config,Nodes,CfgDir,Files}|Ts],Spec) when is_list(Nodes); + Nodes == all_nodes -> + add_tests([{config,Nodes,{CfgDir,Files}}|Ts],Spec); +add_tests([{config,Node,CfgDir,FileOrFiles}|Ts],Spec) -> + add_tests([{config,Node,{CfgDir,FileOrFiles}}|Ts],Spec); +add_tests([{config,CfgDir=[Ch|_],Files}|Ts],Spec) when is_integer(Ch) -> + add_tests([{config,all_nodes,{CfgDir,Files}}|Ts],Spec); + +add_tests([{event_handler,Nodes,Hs,Args}|Ts],Spec) when is_list(Nodes); + Nodes == all_nodes -> + add_tests([{event_handler,Nodes,{Hs,Args}}|Ts],Spec); +add_tests([{event_handler,Node,HOrHs,Args}|Ts],Spec) -> + add_tests([{event_handler,Node,{HOrHs,Args}}|Ts],Spec); + +add_tests([{enable_builtin_hooks,Bool}|Ts],Spec) -> + add_tests(Ts, Spec#testspec{enable_builtin_hooks = Bool}); + +add_tests([{noinput,Bool}|Ts],Spec) -> + add_tests(Ts, Spec#testspec{noinput = Bool}); + %% --- handled/errors --- +add_tests([{define,_,_}|Ts],Spec) -> % handled + add_tests(Ts,Spec); + add_tests([{alias,_,_}|Ts],Spec) -> % handled add_tests(Ts,Spec); add_tests([{node,_,_}|Ts],Spec) -> % handled add_tests(Ts,Spec); -add_tests([{merge_tests, _} | Ts], Spec) -> % handled +add_tests([{merge_tests, _} | Ts], Spec) -> % handled add_tests(Ts,Spec); -%% check if it's a CT term that has bad format or if the user seems to -%% have added something of his/her own, which we'll let pass if relaxed -%% mode is enabled. -add_tests([Other|Ts],Spec) when is_tuple(Other) -> - [Name|_] = tuple_to_list(Other), - case lists:keymember(Name,1,valid_terms()) of - true -> % halt - throw({error,{bad_term_in_spec,Other}}); - false -> % ignore - case get(relaxed) of - true -> - %% warn if name resembles a CT term - case resembles_ct_term(Name,size(Other)) of - true -> - io:format("~nSuspicious term, please check:~n" - "~p~n", [Other]); - false -> - ok - end, - add_tests(Ts,Spec); - false -> - throw({error,{undefined_term_in_spec,Other}}) - end +%% -------------------------------------------------- +%% / \ +%% | General add_tests/2 clauses below will work for | +%% | most test spec configuration terms | +%% \ / +%% -------------------------------------------------- + +%% create one test entry per known node and reinsert +add_tests([Term={Tag,all_nodes,Data}|Ts],Spec) -> + case check_term(Term) of + valid -> + Tests = [{Tag,Node,Data} || Node <- list_nodes(Spec), + should_be_added(Tag,Node,Data,Spec)], + add_tests(Tests++Ts,Spec); + invalid -> % ignore term + add_tests(Ts,Spec) end; - +%% create one test entry per node in Nodes and reinsert +add_tests([{Tag,[],Data}|Ts],Spec) -> + add_tests([{Tag,all_nodes,Data}|Ts],Spec); +add_tests([{Tag,String=[Ch|_],Data}|Ts],Spec) when is_integer(Ch) -> + add_tests([{Tag,all_nodes,{String,Data}}|Ts],Spec); +add_tests([{Tag,NodesOrOther,Data}|Ts],Spec) when is_list(NodesOrOther) -> + case lists:all(fun(Test) -> is_node(Test,Spec#testspec.nodes) + end, NodesOrOther) of + true -> + Ts1 = separate(NodesOrOther,Tag,[Data],Ts,Spec#testspec.nodes), + add_tests(Ts1,Spec); + false -> + add_tests([{Tag,all_nodes,{NodesOrOther,Data}}|Ts],Spec) + end; +%% update data for testspec term of type Tag +add_tests([Term={Tag,NodeOrOther,Data}|Ts],Spec) -> + case is_node(NodeOrOther,Spec#testspec.nodes) of + true -> + case check_term(Term) of + valid -> + Node = ref2node(NodeOrOther,Spec#testspec.nodes), + NodeIxData = + update_recorded(Tag,Node,Spec) ++ + handle_data(Tag,Node,Data,Spec), + add_tests(Ts,mod_field(Spec,Tag,NodeIxData)); + invalid -> % ignore term + add_tests(Ts,Spec) + end; + false -> + add_tests([{Tag,all_nodes,{NodeOrOther,Data}}|Ts],Spec) + end; +%% this test should be added for all known nodes +add_tests([Term={Tag,Data}|Ts],Spec) -> + case check_term(Term) of + valid -> + add_tests([{Tag,all_nodes,Data}|Ts],Spec); + invalid -> + add_tests(Ts,Spec) + end; +%% some other data than a tuple add_tests([Other|Ts],Spec) -> case get(relaxed) of true -> @@ -906,6 +863,105 @@ add_tests([Other|Ts],Spec) -> add_tests([],Spec) -> % done Spec. +%% check if it's a CT term that has bad format or if the user seems to +%% have added something of his/her own, which we'll let pass if relaxed +%% mode is enabled. +check_term(Term) -> + Size = size(Term), + [Name|_] = tuple_to_list(Term), + Valid = valid_terms(), + case lists:member({Name,Size},Valid) of + true -> + valid; + false -> + case lists:keymember(Name,1,Valid) of + true -> % halt + throw({error,{bad_term_in_spec,Term}}); + false -> % ignore + case get(relaxed) of + true -> + %% warn if name resembles a CT term + case resembles_ct_term(Name,size(Term)) of + true -> + io:format("~nSuspicious term, " + "please check:~n" + "~p~n", [Term]), + invalid; + false -> + invalid + end; + false -> + throw({error,{undefined_term_in_spec,Term}}) + end + end + end. + +%% specific data handling before saving in testspec record, e.g. +%% converting relative paths to absolute for directories and files +%% (introduce a clause *only* if the data value needs processing) +handle_data(logdir,Node,Dir,Spec) -> + [{Node,ref2dir(Dir,Spec)}]; +handle_data(cover,Node,File,Spec) -> + [{Node,get_absfile(File,Spec)}]; +handle_data(include,Node,Dirs=[D|_],Spec) when is_list(D) -> + [{Node,ref2dir(Dir,Spec)} || Dir <- Dirs]; +handle_data(include,Node,Dir=[Ch|_],Spec) when is_integer(Ch) -> + handle_data(include,Node,[Dir],Spec); +handle_data(config,Node,File=[Ch|_],Spec) when is_integer(Ch) -> + handle_data(config,Node,[File],Spec); +handle_data(config,Node,{CfgDir,File=[Ch|_]},Spec) when is_integer(Ch) -> + handle_data(config,Node,{CfgDir,[File]},Spec); +handle_data(config,Node,Files=[F|_],Spec) when is_list(F) -> + [{Node,get_absfile(File,Spec)} || File <- Files]; +handle_data(config,Node,{CfgDir,Files=[F|_]},Spec) when is_list(F) -> + [{Node,filename:join(ref2dir(CfgDir,Spec),File)} || File <- Files]; +handle_data(userconfig,Node,CBs,Spec) when is_list(CBs) -> + [{Node,{Callback,get_absfile(Callback,Config,Spec)}} || + {Callback,Config} <- CBs]; +handle_data(userconfig,Node,CB,Spec) when is_tuple(CB) -> + handle_data(userconfig,Node,[CB],Spec); +handle_data(event_handler,Node,H,Spec) when is_atom(H) -> + handle_data(event_handler,Node,{[H],[]},Spec); +handle_data(event_handler,Node,{H,Args},Spec) when is_atom(H) -> + handle_data(event_handler,Node,{[H],Args},Spec); +handle_data(event_handler,Node,Hs,_Spec) when is_list(Hs) -> + [{Node,EvH,[]} || EvH <- Hs]; +handle_data(event_handler,Node,{Hs,Args},_Spec) when is_list(Hs) -> + [{Node,EvH,Args} || EvH <- Hs]; +handle_data(ct_hooks,Node,Hooks,_Spec) when is_list(Hooks) -> + [{Node,Hook} || Hook <- Hooks ]; +handle_data(ct_hooks,Node,Hook,_Spec) -> + [{Node,Hook}]; +handle_data(stylesheet,Node,CSSFile,Spec) -> + [{Node,get_absfile(CSSFile,Spec)}]; +handle_data(_Tag,Node,Data,_Spec) -> + [{Node,Data}]. + +%% check if duplicates should be saved or not +should_be_added(Tag,Node,_Data,Spec) -> + if + %% list terms *without* possible duplicates here + Tag == logdir; Tag == logopts; + Tag == basic_html; Tag == label; + Tag == auto_compile; Tag == stylesheet -> + lists:keymember(ref2node(Node,Spec#testspec.nodes),1, + read_field(Spec,Tag)) == false; + %% for terms *with* possible duplicates + true -> + true + end. + +%% check if previous elements for Node should be deleted +update_recorded(Tag,Node,Spec) -> + if Tag == config; Tag == userconfig; Tag == event_handler; + Tag == ct_hooks; Tag == include -> + read_field(Spec,Tag); + true -> + %% delete previous value for Tag + lists:keydelete(Node,1,read_field(Spec,Tag)) + end. + +%% create one test term per node separate(Nodes,Tag,Data,Tests,Refs) -> Separated = separate(Nodes,Tag,Data,Refs), Separated ++ Tests. @@ -913,7 +969,25 @@ separate([N|Ns],Tag,Data,Refs) -> [list_to_tuple([Tag,ref2node(N,Refs)|Data])|separate(Ns,Tag,Data,Refs)]; separate([],_,_,_) -> []. - + +%% read the value for FieldName in record Rec#testspec +read_field(Rec, FieldName) -> + catch lists:foldl(fun(F, Pos) when F == FieldName -> + throw(element(Pos, Rec)); + (_,Pos) -> + Pos+1 + end,2,?testspec_fields). + +%% modify the value for FieldName in record Rec#testspec +mod_field(Rec, FieldName, NewVal) -> + [_testspec|RecList] = tuple_to_list(Rec), + RecList1 = + (catch lists:foldl(fun(F, {Prev,[_OldVal|Rest]}) when F == FieldName -> + throw(lists:reverse(Prev) ++ [NewVal|Rest]); + (_,{Prev,[Field|Rest]}) -> + {[Field|Prev],Rest} + end,{[],RecList},?testspec_fields)), + list_to_tuple([testspec|RecList1]). %% Representation: %% {{Node,Dir},[{Suite1,[GrOrCase11,GrOrCase12,...]}, @@ -1121,33 +1195,40 @@ ref2node(all_nodes,_Refs) -> ref2node(master,_Refs) -> master; ref2node(RefOrNode,Refs) -> - case string:chr(atom_to_list(RefOrNode),$@) of - 0 -> % a ref + case lists:member($@,atom_to_list(RefOrNode)) of + false -> % a ref case lists:keysearch(RefOrNode,1,Refs) of {value,{RefOrNode,Node}} -> Node; false -> throw({error,{noderef_missing,RefOrNode}}) end; - _ -> % a node + true -> % a node RefOrNode end. -ref2dir(Ref,Refs) when is_atom(Ref) -> +ref2dir(Ref,Spec) -> + ref2dir(Ref,Spec#testspec.alias,Spec). + +ref2dir(Ref,Refs,Spec) when is_atom(Ref) -> case lists:keysearch(Ref,1,Refs) of {value,{Ref,Dir}} -> - Dir; + get_absdir(Dir,Spec); false -> throw({error,{alias_missing,Ref}}) end; -ref2dir(Dir,_) when is_list(Dir) -> - Dir. - -is_noderef(What,Nodes) when is_atom(What) -> - is_noderef([What],Nodes); -is_noderef([master|_],_Nodes) -> +ref2dir(Dir,_,Spec) when is_list(Dir) -> + get_absdir(Dir,Spec); +ref2dir(What,_,_) -> + throw({error,{invalid_directory_name,What}}). + +is_node(What,Nodes) when is_atom(What) -> + is_node([What],Nodes); +is_node([master|_],_Nodes) -> true; -is_noderef([What|_],Nodes) -> +is_node(What={N,H},Nodes) when is_atom(N), is_atom(H) -> + is_node([What],Nodes); +is_node([What|_],Nodes) -> case lists:keymember(What,1,Nodes) or lists:keymember(What,2,Nodes) of true -> @@ -1155,24 +1236,28 @@ is_noderef([What|_],Nodes) -> false -> false end; -is_noderef([],_) -> +is_node([],_) -> false. valid_terms() -> [ + {define,3}, {node,3}, {cover,2}, {cover,3}, {config,2}, {config,3}, + {config,4}, {userconfig,2}, {userconfig,3}, {alias,3}, - {merge_tests,1}, + {merge_tests,2}, {logdir,2}, {logdir,3}, {logopts,2}, {logopts,3}, + {basic_html,2}, + {basic_html,3}, {verbosity,2}, {verbosity,3}, {label,2}, @@ -1182,13 +1267,18 @@ valid_terms() -> {event_handler,4}, {ct_hooks,2}, {ct_hooks,3}, - {enable_builtin_hooks,1}, + {enable_builtin_hooks,2}, + {noinput,2}, {multiply_timetraps,2}, {multiply_timetraps,3}, {scale_timetraps,2}, {scale_timetraps,3}, {include,2}, {include,3}, + {auto_compile,2}, + {auto_compile,3}, + {stylesheet,2}, + {stylesheet,3}, {suites,3}, {suites,4}, {groups,4}, @@ -1203,7 +1293,8 @@ valid_terms() -> {skip_groups,7}, {skip_cases,5}, {skip_cases,6}, - {create_priv_dir,2} + {create_priv_dir,2}, + {create_priv_dir,3} ]. %% this function "guesses" if the user has misspelled a term name diff --git a/lib/common_test/src/ct_util.erl b/lib/common_test/src/ct_util.erl index 4b19062697..990a8ae6ef 100644 --- a/lib/common_test/src/ct_util.erl +++ b/lib/common_test/src/ct_util.erl @@ -377,11 +377,23 @@ loop(Mode,TestData,StartDir) -> {'EXIT',_Pid,normal} -> loop(Mode,TestData,StartDir); {'EXIT',Pid,Reason} -> - %% Let process crash in case of error, this shouldn't happen! - io:format("\n\nct_util_server got EXIT from ~p: ~p\n\n", - [Pid,Reason]), - file:set_cwd(StartDir), - exit(Reason) + case ets:lookup(?conn_table,Pid) of + [#conn{address=A,callback=CB}] -> + %% A connection crashed - remove the connection but don't die + ct_logs:tc_log_async(ct_error_notify, + "Connection process died: " + "Pid: ~p, Address: ~p, Callback: ~p\n" + "Reason: ~p\n\n", + [Pid,A,CB,Reason]), + catch CB:close(Pid), + loop(Mode,TestData,StartDir); + _ -> + %% Let process crash in case of error, this shouldn't happen! + io:format("\n\nct_util_server got EXIT from ~p: ~p\n\n", + [Pid,Reason]), + file:set_cwd(StartDir), + exit(Reason) + end end. diff --git a/lib/common_test/src/ct_util.hrl b/lib/common_test/src/ct_util.hrl index 48e3027f0a..7015014441 100644 --- a/lib/common_test/src/ct_util.hrl +++ b/lib/common_test/src/ct_util.hrl @@ -34,6 +34,7 @@ profile=[], logdir=["."], logopts=[], + basic_html=[], verbosity=[], cover=[], config=[], @@ -41,7 +42,10 @@ event_handler=[], ct_hooks=[], enable_builtin_hooks=true, + noinput=false, include=[], + auto_compile=[], + stylesheet=[], multiply_timetraps=[], scale_timetraps=[], create_priv_dir=[], @@ -65,3 +69,11 @@ -define(ct_config_txt, ct_config_plain). -define(ct_profile_file, ".common_test"). + +-define(css_default, "ct_default.css"). +-define(sortable_table_name, "SortableTable"). +-define(jquery_script, "jquery-latest.js"). +-define(tablesorter_script, "jquery.tablesorter.min.js"). + +%% Logging information for error handler +-record(conn_log, {client, name, address, action, module}). diff --git a/lib/common_test/src/cth_conn_log.erl b/lib/common_test/src/cth_conn_log.erl new file mode 100644 index 0000000000..3af89db3a5 --- /dev/null +++ b/lib/common_test/src/cth_conn_log.erl @@ -0,0 +1,124 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2012. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +%%---------------------------------------------------------------------- +%% CT hook for logging of connections. +%% +%% HookOptions can be hardcoded in the test suite: +%% +%% suite() -> +%% [{ct_hooks, [{cth_conn_log, +%% [{ct_netconfc:conn_mod(),ct_netconfc:hook_options()}]}]}]. +%% +%% or specified in a configuration file: +%% +%% {ct_conn_log,[{ct_netconfc:conn_mod(),ct_netconfc:hook_options()}]}. +%% +%% The conn_mod() is the common test module implementing the protocol, +%% e.g. ct_netconfc, ct_telnet, etc. This module must log by calling +%% +%% error_logger:info_report(ConnLogInfo,Data). +%% ConnLogInfo = #conn_log{} | {ct_connection,Action,ConnName} +%% Action = open | close | send | recv | term() +%% ConnName = atom() - The 'KeyOrName' argument used when opening the connection +%% +%% ct_conn_log_h will print to html log or separate file (depending on +%% log_type() option). conn_mod() must implement and export +%% +%% format_data(log_type(), Data). +%% +%% If logging to separate file, ct_conn_log_h will also log error +%% reports which are witten like this: +%% +%% error_logger:error_report([{ct_connection,ConnName} | Report]). +%% +%%---------------------------------------------------------------------- +-module(cth_conn_log). + +-include_lib("common_test/include/ct.hrl"). + +-export([init/2, + pre_init_per_testcase/3, + post_end_per_testcase/4]). + +-spec init(Id, HookOpts) -> Result when + Id :: term(), + HookOpts :: ct:hook_options(), + Result :: {ok,[{ct_netconfc:conn_mod(), + {ct_netconfc:log_type(),[ct_netconfc:key_or_name()]}}]}. +init(_Id, HookOpts) -> + ConfOpts = ct:get_config(ct_conn_log,[]), + {ok,merge_log_info(ConfOpts,HookOpts)}. + +merge_log_info([{Mod,ConfOpts}|ConfList],HookList) -> + {Opts,HookList1} = + case lists:keytake(Mod,1,HookList) of + false -> + {ConfOpts,HookList}; + {value,{_,HookOpts},HL1} -> + {ConfOpts ++ HookOpts, HL1} % ConfOpts overwrites HookOpts! + end, + [{Mod,get_log_opts(Opts)} | merge_log_info(ConfList,HookList1)]; +merge_log_info([],HookList) -> + [{Mod,get_log_opts(Opts)} || {Mod,Opts} <- HookList]. + +get_log_opts(Opts) -> + LogType = proplists:get_value(log_type,Opts,html), + Hosts = proplists:get_value(hosts,Opts,[]), + {LogType,Hosts}. + + +pre_init_per_testcase(TestCase,Config,CthState) -> + Logs = + lists:map( + fun({ConnMod,{LogType,Hosts}}) -> + case LogType of + LogType when LogType==raw; LogType==pretty -> + Dir = ?config(priv_dir,Config), + TCStr = atom_to_list(TestCase), + ConnModStr = atom_to_list(ConnMod), + DefLogName = TCStr ++ "-" ++ ConnModStr ++ ".txt", + DefLog = filename:join(Dir,DefLogName), + Ls = [{Host, + filename:join(Dir,TCStr ++ "-"++ + atom_to_list(Host) ++ "-" ++ + ConnModStr ++ + ".txt")} + || Host <- Hosts] + ++[{default,DefLog}], + Str = + "<table borders=1>" + "<b>" ++ ConnModStr ++ " logs:</b>\n" ++ + [io_lib:format( + "<tr><td>~p</td><td><a href=~p>~s</a></td></tr>", + [S,L,filename:basename(L)]) + || {S,L} <- Ls] ++ + "</table>", + io:format(Str,[]), + {ConnMod,{LogType,Ls}}; + _ -> + {ConnMod,{LogType,[]}} + end + end, + CthState), + error_logger:add_report_handler(ct_conn_log_h,{group_leader(),Logs}), + {Config,CthState}. + +post_end_per_testcase(_TestCase,_Config,Return,CthState) -> + error_logger:delete_report_handler(ct_conn_log_h), + {Return,CthState}. diff --git a/lib/common_test/src/cth_surefire.erl b/lib/common_test/src/cth_surefire.erl index c42f956b3a..e7bd84e51b 100644 --- a/lib/common_test/src/cth_surefire.erl +++ b/lib/common_test/src/cth_surefire.erl @@ -49,9 +49,12 @@ init(Path, Opts) -> properties = proplists:get_value(properties,Opts,[]), timer = now() }. -pre_init_per_suite(Suite,Config,State) -> +pre_init_per_suite(Suite,Config,#state{ test_cases = [] } = State) -> {Config, init_tc(State#state{ curr_suite = Suite, curr_suite_ts = now() }, - Config) }. + Config) }; +pre_init_per_suite(Suite,Config,State) -> + %% Have to close the previous suite + pre_init_per_suite(Suite,Config,close_suite(State)). post_init_per_suite(_Suite,Config, Result, State) -> {Result, end_tc(init_per_suite,Config,Result,State)}. @@ -59,11 +62,7 @@ post_init_per_suite(_Suite,Config, Result, State) -> pre_end_per_suite(_Suite,Config,State) -> {Config, init_tc(State, Config)}. post_end_per_suite(_Suite,Config,Result,State) -> - NewState = end_tc(end_per_suite,Config,Result,State), - TCs = NewState#state.test_cases, - Suite = get_suite(NewState, TCs), - {Result, State#state{ test_cases = [], - test_suites = [Suite | State#state.test_suites]}}. + {Result, end_tc(end_per_suite,Config,Result,State)}. pre_init_per_group(Group,Config,State) -> {Config, init_tc(State#state{ curr_group = [Group|State#state.curr_group]}, @@ -83,24 +82,36 @@ pre_init_per_testcase(_TC,Config,State) -> {Config, init_tc(State, Config)}. post_end_per_testcase(TC,Config,Result,State) -> {Result, end_tc(TC,Config, Result,State)}. +on_tc_fail(_TC, _Res, State = #state{test_cases = []}) -> + State; on_tc_fail(_TC, Res, State) -> TCs = State#state.test_cases, - TC = hd(State#state.test_cases), - NewTC = TC#testcase{ failure = - {fail,lists:flatten(io_lib:format("~p",[Res]))} }, + TC = hd(TCs), + NewTC = TC#testcase{ + failure = + {fail,lists:flatten(io_lib:format("~p",[Res]))} }, State#state{ test_cases = [NewTC | tl(TCs)]}. +on_tc_skip(Tc,{Type,Reason} = Res, State) when Type == tc_auto_skip -> + do_tc_skip(Res, end_tc(Tc,[],Res,init_tc(State,[]))); +on_tc_skip(_Tc, _Res, State = #state{test_cases = []}) -> + State; on_tc_skip(_Tc, Res, State) -> + do_tc_skip(Res, State). + +do_tc_skip(Res, State) -> TCs = State#state.test_cases, - TC = hd(State#state.test_cases), + TC = hd(TCs), NewTC = TC#testcase{ failure = {skipped,lists:flatten(io_lib:format("~p",[Res]))} }, State#state{ test_cases = [NewTC | tl(TCs)]}. +init_tc(State, Config) when is_list(Config) == false -> + State#state{ timer = now(), tc_log = "" }; init_tc(State, Config) -> State#state{ timer = now(), - tc_log = proplists:get_value(tc_logfile, Config)}. + tc_log = proplists:get_value(tc_logfile, Config, [])}. end_tc(Func, Config, Res, State) when is_atom(Func) -> end_tc(atom_to_list(Func), Config, Res, State); @@ -118,26 +129,35 @@ end_tc(Name, _Config, _Res, State = #state{ curr_suite = Suite, name = Name, time = TimeTakes, failure = passed }| State#state.test_cases]}. - -get_suite(State, TCs) -> +close_suite(#state{ test_cases = [] } = State) -> + State; +close_suite(#state{ test_cases = TCs } = State) -> Total = length(TCs), Succ = length(lists:filter(fun(#testcase{ failure = F }) -> F == passed end,TCs)), Fail = Total - Succ, TimeTaken = timer:now_diff(now(),State#state.curr_suite_ts) / 1000000, - #testsuite{ name = atom_to_list(State#state.curr_suite), - package = State#state.package, - time = io_lib:format("~f",[TimeTaken]), - timestamp = now_to_string(State#state.curr_suite_ts), - errors = Fail, tests = Total, testcases = lists:reverse(TCs) }. - -terminate(State) -> - {ok,D} = file:open(State#state.filepath,[write]), + Suite = #testsuite{ name = atom_to_list(State#state.curr_suite), + package = State#state.package, + time = io_lib:format("~f",[TimeTaken]), + timestamp = now_to_string(State#state.curr_suite_ts), + errors = Fail, tests = Total, + testcases = lists:reverse(TCs) }, + State#state{ test_cases = [], + test_suites = [Suite | State#state.test_suites]}. + +terminate(State = #state{ test_cases = [] }) -> + {ok,D} = file:open(State#state.filepath,[write,{encoding,utf8}]), io:format(D, "<?xml version=\"1.0\" encoding= \"UTF-8\" ?>", []), io:format(D, to_xml(State), []), catch file:sync(D), - catch file:close(D). + catch file:close(D); +terminate(State) -> + %% Have to close the last suite + terminate(close_suite(State)). + + to_xml(#testcase{ group = Group, classname = CL, log = L, name = N, time = T, timestamp = TS, failure = F}) -> ["<testcase ", diff --git a/lib/common_test/test/Makefile b/lib/common_test/test/Makefile index 46df2b3e22..7628ada61a 100644 --- a/lib/common_test/test/Makefile +++ b/lib/common_test/test/Makefile @@ -39,6 +39,7 @@ MODULES= \ ct_sequence_1_SUITE \ ct_repeat_1_SUITE \ ct_testspec_1_SUITE \ + ct_testspec_2_SUITE \ ct_skip_SUITE \ ct_error_SUITE \ ct_test_server_if_1_SUITE \ @@ -46,6 +47,9 @@ MODULES= \ ct_master_SUITE \ ct_misc_1_SUITE \ ct_hooks_SUITE \ + ct_netconfc_SUITE \ + ct_basic_html_SUITE \ + ct_auto_compile_SUITE \ ct_verbosity_SUITE ERL_FILES= $(MODULES:%=%.erl) diff --git a/lib/common_test/test/common_test.spec b/lib/common_test/test/common_test.spec index 8755b08117..8bec66d6f2 100644 --- a/lib/common_test/test/common_test.spec +++ b/lib/common_test/test/common_test.spec @@ -1 +1 @@ -{suites,"../common_test_test",all}.
\ No newline at end of file +{suites,"../common_test_test",all}. diff --git a/lib/common_test/test/ct_auto_compile_SUITE.erl b/lib/common_test/test/ct_auto_compile_SUITE.erl new file mode 100644 index 0000000000..cc546ed30d --- /dev/null +++ b/lib/common_test/test/ct_auto_compile_SUITE.erl @@ -0,0 +1,187 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2009-2012. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%%%------------------------------------------------------------------- +%%% File: ct_auto_compile_SUITE +%%% +%%% Description: +%%% +%%% +%%% The suites used for the test are located in the data directory. +%%%------------------------------------------------------------------- +-module(ct_auto_compile_SUITE). + +-compile(export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("common_test/include/ct_event.hrl"). + +-define(eh, ct_test_support_eh). + +%%-------------------------------------------------------------------- +%% TEST SERVER CALLBACK FUNCTIONS +%%-------------------------------------------------------------------- + +%%-------------------------------------------------------------------- +%% Description: Since Common Test starts another Test Server +%% instance, the tests need to be performed on a separate node (or +%% there will be clashes with logging processes etc). +%%-------------------------------------------------------------------- +init_per_suite(Config) -> + Config1 = ct_test_support:init_per_suite(Config), + Config1. + +end_per_suite(Config) -> + ct_test_support:end_per_suite(Config). + +init_per_testcase(TestCase, Config) -> + ct_test_support:init_per_testcase(TestCase, Config). + +end_per_testcase(TestCase, Config) -> + ct_test_support:end_per_testcase(TestCase, Config). + +suite() -> [{ct_hooks,[ts_install_cth]}]. + +all() -> + [ac_flag, ac_spec]. + +groups() -> + []. + +init_per_group(_GroupName, Config) -> + Config. + +end_per_group(_GroupName, Config) -> + Config. + + + +%%-------------------------------------------------------------------- +%% TEST CASES +%%-------------------------------------------------------------------- + +%%%----------------------------------------------------------------- +%%% +ac_flag(Config) when is_list(Config) -> + DataDir = ?config(data_dir, Config), + PrivDir = ?config(priv_dir, Config), + file:copy(filename:join(DataDir, "bad_SUITE.erl"), + filename:join(PrivDir, "bad_SUITE.erl")), + Suite = filename:join(DataDir, "dummy_SUITE"), + compile:file(Suite, [{outdir,PrivDir}]), + {Opts,ERPid} = setup([{dir,PrivDir}, + {auto_compile,false}, + {label,"ac_flag"}], + Config), + + ok = ct_test_support:run(Opts, Config), + Events = ct_test_support:get_events(ERPid, Config), + + ct_test_support:log_events(ac_flag, + reformat(Events, ?eh), + PrivDir, + Opts), + + TestEvents = events_to_check(ac_flag), + ok = ct_test_support:verify_events(TestEvents, Events, Config). + +%%%----------------------------------------------------------------- +%%% +ac_spec(Config) when is_list(Config) -> + DataDir = ?config(data_dir, Config), + PrivDir = ?config(priv_dir, Config), + file:copy(filename:join(DataDir, "bad_SUITE.erl"), + filename:join(PrivDir, "bad_SUITE.erl")), + TestSpec = [{label,ac_spec}, + {auto_compile,false}, + {suites,PrivDir,all}], + FileName = filename:join(?config(priv_dir, Config),"ac_spec.spec"), + {ok,Dev} = file:open(FileName, [write]), + [io:format(Dev, "~p.~n", [Term]) || Term <- TestSpec], + file:close(Dev), + + {Opts,ERPid} = setup([{spec,FileName}], Config), + ok = ct_test_support:run(Opts, Config), + Events = ct_test_support:get_events(ERPid, Config), + + ct_test_support:log_events(ac_spec, + reformat(Events, ?eh), + PrivDir, + Opts), + + TestEvents = events_to_check(ac_spec), + ok = ct_test_support:verify_events(TestEvents, Events, Config). + + +%%%----------------------------------------------------------------- +%%% HELP FUNCTIONS +%%%----------------------------------------------------------------- + +setup(Test, Config) -> + Opts0 = ct_test_support:get_opts(Config), + Level = ?config(trace_level, Config), + EvHArgs = [{cbm,ct_test_support},{trace_level,Level}], + Opts = Opts0 ++ [{event_handler,{?eh,EvHArgs}}|Test], + ERPid = ct_test_support:start_event_receiver(Config), + {Opts,ERPid}. + +reformat(Events, EH) -> + ct_test_support:reformat(Events, EH). + %reformat(Events, _EH) -> + % Events. + +%%%----------------------------------------------------------------- +%%% TEST EVENTS +%%%----------------------------------------------------------------- +events_to_check(Test) -> + %% 2 tests (ct:run_test + script_start) is default + events_to_check(Test, 2). + +events_to_check(_, 0) -> + []; +events_to_check(Test, N) -> + test_events(Test) ++ events_to_check(Test, N-1). + +test_events(ac_flag) -> + [ + {ct_test_support_eh,start_logging,{'DEF','RUNDIR'}}, + {ct_test_support_eh,test_start,{'DEF',{'START_TIME','LOGDIR'}}}, + {ct_test_support_eh,start_info,{1,1,3}}, + {ct_test_support_eh,tc_start,{dummy_SUITE,init_per_suite}}, + {ct_test_support_eh,tc_done,{dummy_SUITE,init_per_suite,ok}}, + {ct_test_support_eh,test_stats,{1,1,{1,0}}}, + {ct_test_support_eh,tc_start,{dummy_SUITE,end_per_suite}}, + {ct_test_support_eh,tc_done,{dummy_SUITE,end_per_suite,ok}}, + {ct_test_support_eh,test_done,{'DEF','STOP_TIME'}}, + {ct_test_support_eh,stop_logging,[]} + ]; + +test_events(ac_spec) -> + [ + {ct_test_support_eh,start_logging,{'DEF','RUNDIR'}}, + {ct_test_support_eh,test_start,{'DEF',{'START_TIME','LOGDIR'}}}, + {ct_test_support_eh,start_info,{1,1,3}}, + {ct_test_support_eh,tc_start,{dummy_SUITE,init_per_suite}}, + {ct_test_support_eh,tc_done,{dummy_SUITE,init_per_suite,ok}}, + {ct_test_support_eh,test_stats,{1,1,{1,0}}}, + {ct_test_support_eh,tc_start,{dummy_SUITE,end_per_suite}}, + {ct_test_support_eh,tc_done,{dummy_SUITE,end_per_suite,ok}}, + {ct_test_support_eh,test_done,{'DEF','STOP_TIME'}}, + {ct_test_support_eh,stop_logging,[]} + ]. diff --git a/lib/common_test/test/ct_auto_compile_SUITE_data/bad_SUITE.erl b/lib/common_test/test/ct_auto_compile_SUITE_data/bad_SUITE.erl new file mode 100644 index 0000000000..6ebcb3570e --- /dev/null +++ b/lib/common_test/test/ct_auto_compile_SUITE_data/bad_SUITE.erl @@ -0,0 +1,23 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2009-2010. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% + +-module(bad_SUITE). + +-compile(export_all). + +bad_bad_suite diff --git a/lib/common_test/test/ct_auto_compile_SUITE_data/dummy_SUITE.erl b/lib/common_test/test/ct_auto_compile_SUITE_data/dummy_SUITE.erl new file mode 100644 index 0000000000..0b1eafc31d --- /dev/null +++ b/lib/common_test/test/ct_auto_compile_SUITE_data/dummy_SUITE.erl @@ -0,0 +1,130 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2009-2010. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% + +-module(dummy_SUITE). + +-compile(export_all). + +-include_lib("common_test/include/ct.hrl"). + +%%-------------------------------------------------------------------- +%% @spec suite() -> Info +%% Info = [tuple()] +%% @end +%%-------------------------------------------------------------------- +suite() -> + [{timetrap,{seconds,30}}]. + +%%-------------------------------------------------------------------- +%% @spec init_per_suite(Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% Config0 = Config1 = [tuple()] +%% Reason = term() +%% @end +%%-------------------------------------------------------------------- +init_per_suite(Config) -> + Config. + +%%-------------------------------------------------------------------- +%% @spec end_per_suite(Config0) -> void() | {save_config,Config1} +%% Config0 = Config1 = [tuple()] +%% @end +%%-------------------------------------------------------------------- +end_per_suite(_Config) -> + ok. + +%%-------------------------------------------------------------------- +%% @spec init_per_group(GroupName, Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% GroupName = atom() +%% Config0 = Config1 = [tuple()] +%% Reason = term() +%% @end +%%-------------------------------------------------------------------- +init_per_group(_GroupName, Config) -> + Config. + +%%-------------------------------------------------------------------- +%% @spec end_per_group(GroupName, Config0) -> +%% void() | {save_config,Config1} +%% GroupName = atom() +%% Config0 = Config1 = [tuple()] +%% @end +%%-------------------------------------------------------------------- +end_per_group(_GroupName, _Config) -> + ok. + +%%-------------------------------------------------------------------- +%% @spec init_per_testcase(TestCase, Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% TestCase = atom() +%% Config0 = Config1 = [tuple()] +%% Reason = term() +%% @end +%%-------------------------------------------------------------------- +init_per_testcase(_TestCase, Config) -> + Config. + +%%-------------------------------------------------------------------- +%% @spec end_per_testcase(TestCase, Config0) -> +%% void() | {save_config,Config1} | {fail,Reason} +%% TestCase = atom() +%% Config0 = Config1 = [tuple()] +%% Reason = term() +%% @end +%%-------------------------------------------------------------------- +end_per_testcase(_TestCase, _Config) -> + ok. + +%%-------------------------------------------------------------------- +%% @spec groups() -> [Group] +%% Group = {GroupName,Properties,GroupsAndTestCases} +%% GroupName = atom() +%% Properties = [parallel | sequence | Shuffle | {RepeatType,N}] +%% GroupsAndTestCases = [Group | {group,GroupName} | TestCase] +%% TestCase = atom() +%% Shuffle = shuffle | {shuffle,{integer(),integer(),integer()}} +%% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail | +%% repeat_until_any_ok | repeat_until_any_fail +%% N = integer() | forever +%% @end +%%-------------------------------------------------------------------- +groups() -> + []. + +%%-------------------------------------------------------------------- +%% @spec all() -> GroupsAndTestCases | {skip,Reason} +%% GroupsAndTestCases = [{group,GroupName} | TestCase] +%% GroupName = atom() +%% TestCase = atom() +%% Reason = term() +%% @end +%%-------------------------------------------------------------------- +all() -> + [ok,fail,skip]. + + +ok(_Config) -> + ok. + +fail(Config) -> + tuple_to_list(Config), + ok. + +skip(_Config) -> + {skip,"should be skipped"}. diff --git a/lib/common_test/test/ct_basic_html_SUITE.erl b/lib/common_test/test/ct_basic_html_SUITE.erl new file mode 100644 index 0000000000..a5f2e6197e --- /dev/null +++ b/lib/common_test/test/ct_basic_html_SUITE.erl @@ -0,0 +1,180 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2009-2012. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%%%------------------------------------------------------------------- +%%% File: ct_basic_html_SUITE +%%% +%%% Description: +%%% +%%% +%%% The suites used for the test are located in the data directory. +%%%------------------------------------------------------------------- +-module(ct_basic_html_SUITE). + +-compile(export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("common_test/include/ct_event.hrl"). + +-define(eh, ct_test_support_eh). + +%%-------------------------------------------------------------------- +%% TEST SERVER CALLBACK FUNCTIONS +%%-------------------------------------------------------------------- + +%%-------------------------------------------------------------------- +%% Description: Since Common Test starts another Test Server +%% instance, the tests need to be performed on a separate node (or +%% there will be clashes with logging processes etc). +%%-------------------------------------------------------------------- +init_per_suite(Config) -> + Config1 = ct_test_support:init_per_suite(Config), + Config1. + +end_per_suite(Config) -> + ct_test_support:end_per_suite(Config). + +init_per_testcase(TestCase, Config) -> + ct_test_support:init_per_testcase(TestCase, Config). + +end_per_testcase(TestCase, Config) -> + ct_test_support:end_per_testcase(TestCase, Config). + +suite() -> [{ct_hooks,[ts_install_cth]}]. + +all() -> + [basic_flag, basic_spec]. + +groups() -> + []. + +init_per_group(_GroupName, Config) -> + Config. + +end_per_group(_GroupName, Config) -> + Config. + + + +%%-------------------------------------------------------------------- +%% TEST CASES +%%-------------------------------------------------------------------- + +%%%----------------------------------------------------------------- +%%% +basic_flag(Config) when is_list(Config) -> + DataDir = ?config(data_dir, Config), + Suites = [filename:join(DataDir, "babbling_SUITE")], + {Opts,ERPid} = setup([{suite,Suites}, + {basic_html,true}, + {label,"basic_flag"}], + Config), + + ok = ct_test_support:run(Opts, Config), + Events = ct_test_support:get_events(ERPid, Config), + + ct_test_support:log_events(basic_flag, + reformat(Events, ?eh), + ?config(priv_dir, Config), + Opts), + + TestEvents = events_to_check(basic_flag), + ok = ct_test_support:verify_events(TestEvents, Events, Config). + +%%%----------------------------------------------------------------- +%%% +basic_spec(Config) when is_list(Config) -> + DataDir = ?config(data_dir, Config), + TestSpec = [{label,basic_spec}, + {basic_html,true}, + {suites,DataDir,babbling_SUITE}], + FileName = filename:join(?config(priv_dir, Config),"basic_spec.spec"), + {ok,Dev} = file:open(FileName, [write]), + [io:format(Dev, "~p.~n", [Term]) || Term <- TestSpec], + file:close(Dev), + + {Opts,ERPid} = setup([{spec,FileName}], Config), + ok = ct_test_support:run(Opts, Config), + Events = ct_test_support:get_events(ERPid, Config), + + ct_test_support:log_events(basic_spec, + reformat(Events, ?eh), + ?config(priv_dir, Config), + Opts), + + TestEvents = events_to_check(basic_spec), + ok = ct_test_support:verify_events(TestEvents, Events, Config). + + +%%%----------------------------------------------------------------- +%%% HELP FUNCTIONS +%%%----------------------------------------------------------------- + +setup(Test, Config) -> + Opts0 = ct_test_support:get_opts(Config), + Level = ?config(trace_level, Config), + EvHArgs = [{cbm,ct_test_support},{trace_level,Level}], + Opts = Opts0 ++ [{event_handler,{?eh,EvHArgs}}|Test], + ERPid = ct_test_support:start_event_receiver(Config), + {Opts,ERPid}. + +reformat(Events, EH) -> + ct_test_support:reformat(Events, EH). + %reformat(Events, _EH) -> + % Events. + +%%%----------------------------------------------------------------- +%%% TEST EVENTS +%%%----------------------------------------------------------------- +events_to_check(Test) -> + %% 2 tests (ct:run_test + script_start) is default + events_to_check(Test, 2). + +events_to_check(_, 0) -> + []; +events_to_check(Test, N) -> + test_events(Test) ++ events_to_check(Test, N-1). + +test_events(basic_flag) -> + [ + {ct_test_support_eh,start_logging,{'DEF','RUNDIR'}}, + {ct_test_support_eh,test_start,{'DEF',{'START_TIME','LOGDIR'}}}, + {ct_test_support_eh,start_info,{1,1,3}}, + {ct_test_support_eh,tc_start,{babbling_SUITE,init_per_suite}}, + {ct_test_support_eh,tc_done,{babbling_SUITE,init_per_suite,ok}}, + {ct_test_support_eh,test_stats,{1,1,{1,0}}}, + {ct_test_support_eh,tc_start,{babbling_SUITE,end_per_suite}}, + {ct_test_support_eh,tc_done,{babbling_SUITE,end_per_suite,ok}}, + {ct_test_support_eh,test_done,{'DEF','STOP_TIME'}}, + {ct_test_support_eh,stop_logging,[]} + ]; + +test_events(basic_spec) -> + [ + {ct_test_support_eh,start_logging,{'DEF','RUNDIR'}}, + {ct_test_support_eh,test_start,{'DEF',{'START_TIME','LOGDIR'}}}, + {ct_test_support_eh,start_info,{1,1,3}}, + {ct_test_support_eh,tc_start,{babbling_SUITE,init_per_suite}}, + {ct_test_support_eh,tc_done,{babbling_SUITE,init_per_suite,ok}}, + {ct_test_support_eh,test_stats,{1,1,{1,0}}}, + {ct_test_support_eh,tc_start,{babbling_SUITE,end_per_suite}}, + {ct_test_support_eh,tc_done,{babbling_SUITE,end_per_suite,ok}}, + {ct_test_support_eh,test_done,{'DEF','STOP_TIME'}}, + {ct_test_support_eh,stop_logging,[]} + ]. diff --git a/lib/common_test/test/ct_basic_html_SUITE_data/babbling_SUITE.erl b/lib/common_test/test/ct_basic_html_SUITE_data/babbling_SUITE.erl new file mode 100644 index 0000000000..d67383c606 --- /dev/null +++ b/lib/common_test/test/ct_basic_html_SUITE_data/babbling_SUITE.erl @@ -0,0 +1,130 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2009-2010. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% + +-module(babbling_SUITE). + +-compile(export_all). + +-include_lib("common_test/include/ct.hrl"). + +%%-------------------------------------------------------------------- +%% @spec suite() -> Info +%% Info = [tuple()] +%% @end +%%-------------------------------------------------------------------- +suite() -> + [{timetrap,{seconds,30}}]. + +%%-------------------------------------------------------------------- +%% @spec init_per_suite(Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% Config0 = Config1 = [tuple()] +%% Reason = term() +%% @end +%%-------------------------------------------------------------------- +init_per_suite(Config) -> + Config. + +%%-------------------------------------------------------------------- +%% @spec end_per_suite(Config0) -> void() | {save_config,Config1} +%% Config0 = Config1 = [tuple()] +%% @end +%%-------------------------------------------------------------------- +end_per_suite(_Config) -> + ok. + +%%-------------------------------------------------------------------- +%% @spec init_per_group(GroupName, Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% GroupName = atom() +%% Config0 = Config1 = [tuple()] +%% Reason = term() +%% @end +%%-------------------------------------------------------------------- +init_per_group(_GroupName, Config) -> + Config. + +%%-------------------------------------------------------------------- +%% @spec end_per_group(GroupName, Config0) -> +%% void() | {save_config,Config1} +%% GroupName = atom() +%% Config0 = Config1 = [tuple()] +%% @end +%%-------------------------------------------------------------------- +end_per_group(_GroupName, _Config) -> + ok. + +%%-------------------------------------------------------------------- +%% @spec init_per_testcase(TestCase, Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% TestCase = atom() +%% Config0 = Config1 = [tuple()] +%% Reason = term() +%% @end +%%-------------------------------------------------------------------- +init_per_testcase(_TestCase, Config) -> + Config. + +%%-------------------------------------------------------------------- +%% @spec end_per_testcase(TestCase, Config0) -> +%% void() | {save_config,Config1} | {fail,Reason} +%% TestCase = atom() +%% Config0 = Config1 = [tuple()] +%% Reason = term() +%% @end +%%-------------------------------------------------------------------- +end_per_testcase(_TestCase, _Config) -> + ok. + +%%-------------------------------------------------------------------- +%% @spec groups() -> [Group] +%% Group = {GroupName,Properties,GroupsAndTestCases} +%% GroupName = atom() +%% Properties = [parallel | sequence | Shuffle | {RepeatType,N}] +%% GroupsAndTestCases = [Group | {group,GroupName} | TestCase] +%% TestCase = atom() +%% Shuffle = shuffle | {shuffle,{integer(),integer(),integer()}} +%% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail | +%% repeat_until_any_ok | repeat_until_any_fail +%% N = integer() | forever +%% @end +%%-------------------------------------------------------------------- +groups() -> + []. + +%%-------------------------------------------------------------------- +%% @spec all() -> GroupsAndTestCases | {skip,Reason} +%% GroupsAndTestCases = [{group,GroupName} | TestCase] +%% GroupName = atom() +%% TestCase = atom() +%% Reason = term() +%% @end +%%-------------------------------------------------------------------- +all() -> + [ok,fail,skip]. + + +ok(_Config) -> + ok. + +fail(Config) -> + tuple_to_list(Config), + ok. + +skip(_Config) -> + {skip,"should be skipped"}. diff --git a/lib/common_test/test/ct_misc_1_SUITE.erl b/lib/common_test/test/ct_misc_1_SUITE.erl index cb17af9ab5..d2318de445 100644 --- a/lib/common_test/test/ct_misc_1_SUITE.erl +++ b/lib/common_test/test/ct_misc_1_SUITE.erl @@ -106,7 +106,7 @@ beam_me_up(Config) when is_list(Config) -> {Opts,ERPid} = setup([{suite,Suites},{auto_compile,false}], Config), - ok = ct_test_support:run(ct, run_test, [Opts], Config), + {_Ok,_Fail,_Skip} = ct_test_support:run(ct, run_test, [Opts], Config), Events = ct_test_support:get_events(ERPid, Config), ct_test_support:log_events(beam_me_up, diff --git a/lib/common_test/test/ct_netconfc_SUITE.erl b/lib/common_test/test/ct_netconfc_SUITE.erl new file mode 100644 index 0000000000..e6e8d5b09c --- /dev/null +++ b/lib/common_test/test/ct_netconfc_SUITE.erl @@ -0,0 +1,124 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2009-2012. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%%%------------------------------------------------------------------- +%%% File: ct_netconfc_SUITE +%%% +%%% Description: +%%% Test ct_netconfc module +%%% +%%%------------------------------------------------------------------- +-module(ct_netconfc_SUITE). + +-compile(export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("common_test/include/ct_event.hrl"). + +-define(eh, ct_test_support_eh). + +%%-------------------------------------------------------------------- +%% TEST SERVER CALLBACK FUNCTIONS +%%-------------------------------------------------------------------- + +%%-------------------------------------------------------------------- +%% Description: Since Common Test starts another Test Server +%% instance, the tests need to be performed on a separate node (or +%% there will be clashes with logging processes etc). +%%-------------------------------------------------------------------- +init_per_suite(Config) -> + Config1 = ct_test_support:init_per_suite(Config), + Config1. + +end_per_suite(Config) -> + ct_test_support:end_per_suite(Config). + +init_per_testcase(TestCase, Config) -> + ct_test_support:init_per_testcase(TestCase, Config). + +end_per_testcase(TestCase, Config) -> + ct_test_support:end_per_testcase(TestCase, Config). + +suite() -> [{ct_hooks,[ts_install_cth]}]. + +all() -> + [ + default + ]. + +%%-------------------------------------------------------------------- +%% TEST CASES +%%-------------------------------------------------------------------- + +%%%----------------------------------------------------------------- +%%% +default(Config) when is_list(Config) -> + DataDir = ?config(data_dir, Config), + Suite = filename:join(DataDir, "netconfc1_SUITE"), + CfgFile = filename:join(DataDir, "netconfc1.cfg"), + {Opts,ERPid} = setup([{suite,Suite},{config,CfgFile}, + {label,default}], Config), + + ok = execute(default, Opts, ERPid, Config). + + +%%%----------------------------------------------------------------- +%%% HELP FUNCTIONS +%%%----------------------------------------------------------------- + +setup(Test, Config) -> + Opts0 = ct_test_support:get_opts(Config), + Level = ?config(trace_level, Config), + EvHArgs = [{cbm,ct_test_support},{trace_level,Level}], + Opts = Opts0 ++ [{event_handler,{?eh,EvHArgs}}|Test], + ERPid = ct_test_support:start_event_receiver(Config), + {Opts,ERPid}. + +execute(Name, Opts, ERPid, Config) -> + ok = ct_test_support:run(Opts, Config), + Events = ct_test_support:get_events(ERPid, Config), + + ct_test_support:log_events(Name, + reformat(Events, ?eh), + ?config(priv_dir, Config), + Opts), + + TestEvents = events_to_check(Name,Config), + ct_test_support:verify_events(TestEvents, Events, Config). + +reformat(Events, EH) -> + ct_test_support:reformat(Events, EH). + +%%%----------------------------------------------------------------- +%%% TEST EVENTS +%%%----------------------------------------------------------------- +events_to_check(Test,Config) -> + {module,_} = code:load_abs(filename:join(?config(data_dir,Config), + netconfc1_SUITE)), + TCs = netconfc1_SUITE:all(), + code:purge(netconfc1_SUITE), + code:delete(netconfc1_SUITE), + + OneTest = + [{?eh,start_logging,{'DEF','RUNDIR'}}] ++ + [{?eh,tc_done,{netconfc1_SUITE,TC,ok}} || TC <- TCs] ++ + [{?eh,stop_logging,[]}], + + %% 2 tests (ct:run_test + script_start) is default + OneTest ++ OneTest. diff --git a/lib/common_test/test/ct_netconfc_SUITE_data/netconfc1.cfg b/lib/common_test/test/ct_netconfc_SUITE_data/netconfc1.cfg new file mode 100644 index 0000000000..6466571623 --- /dev/null +++ b/lib/common_test/test/ct_netconfc_SUITE_data/netconfc1.cfg @@ -0,0 +1,6 @@ +%% -*- erlang -*- +{netconf1,[{ssh,"localhost"}, + {port,2060}, + {user,"xxx"}, + {password,"xxx"}]}. +{ct_conn_log,[{ct_netconfc,[{log_type,pretty}]}]}. %overrides args to cth_conn_log diff --git a/lib/common_test/test/ct_netconfc_SUITE_data/netconfc1_SUITE.erl b/lib/common_test/test/ct_netconfc_SUITE_data/netconfc1_SUITE.erl new file mode 100644 index 0000000000..79768a9a6a --- /dev/null +++ b/lib/common_test/test/ct_netconfc_SUITE_data/netconfc1_SUITE.erl @@ -0,0 +1,1130 @@ +%%-------------------------------------------------------------------- +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2012. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +%%---------------------------------------------------------------------- +%% File: ct_netconfc_SUITE.erl +%% +%% Description: +%% This file contains the test cases for the ct_netconfc API. +%% +%% @author Support +%% @doc Netconf Client Interface. +%% @end +%%---------------------------------------------------------------------- +%%---------------------------------------------------------------------- +-module(netconfc1_SUITE). +-include_lib("common_test/include/ct.hrl"). +-include_lib("common_test/src/ct_netconfc.hrl"). +-include_lib("public_key/include/public_key.hrl"). + +-compile(export_all). + +%% Default timetrap timeout (set in init_per_testcase). +-define(default_timeout, ?t:minutes(1)). + +-define(NS,ns). +-define(LOCALHOST, "127.0.0.1"). +-define(SSH_PORT, 2060). + +-define(DEFAULT_SSH_OPTS,[{ssh,?LOCALHOST}, + {port,?SSH_PORT}, + {user,"xxx"}, + {password,"xxx"}]). +-define(DEFAULT_SSH_OPTS(Dir), ?DEFAULT_SSH_OPTS++[{user_dir,Dir}]). + +-define(ok,ok). + +suite() -> + [{ct_hooks, [{cth_conn_log, + [{ct_netconfc,[{log_type,html}, %will be overwritten by config + {hosts,[my_named_connection,netconf1]}] + }] + }] + }]. + +all() -> + case os:find_executable("ssh") of + false -> + {skip, "SSH not installed on host"}; + _ -> + [hello, + hello_from_server_first, + hello_named, + hello_configured, + hello_configured_extraopts, + hello_required, + hello_required_exists, + hello_global_pwd, + hello_no_session_id, + hello_incomp_base_vsn, + hello_no_base_cap, + hello_no_caps, + no_server_hello, + no_client_hello, + get_session_id, + get_capabilities, + faulty_user, + faulty_passwd, + faulty_port, + no_host, + no_port, + invalid_opt, + get, + get_xpath, + get_config, + get_config_xpath, + edit_config, + copy_config, + delete_config, + lock, + unlock, + kill_session, + get_no_such_client, + action, + send_any_rpc, + send_any, + hide_password, + not_proper_xml, + prefixed_namespace, + receive_chunked_data, + timeout_receive_chunked_data, + close_while_waiting_for_chunked_data, + connection_crash, + get_event_streams, + create_subscription, + receive_event] + end. + + +groups() -> + []. + +init_per_group(_GroupName, Config) -> + Config. + +end_per_group(_GroupName, Config) -> + Config. + +init_per_testcase(_Case, Config) -> + ets:delete_all_objects(ns_tab), + Dog = test_server:timetrap(?default_timeout), + [{watchdog, Dog}|Config]. + +end_per_testcase(_Case, Config) -> + Dog=?config(watchdog, Config), + test_server:timetrap_cancel(Dog), + ok. + +init_per_suite(Config) -> + case catch {crypto:start(), ssh:start()} of + {ok, ok} -> + {ok, _} = get_id_keys(Config), + make_dsa_files(Config), + Server = ?NS:start(?config(data_dir,Config)), + [{server,Server}|Config]; + _ -> + {skip, "Crypto and/or SSH could not be started!"} + end. + +end_per_suite(Config) -> + PrivDir = ?config(priv_dir, Config), + ?NS:stop(?config(server,Config)), + ssh:stop(), + crypto:stop(), + remove_id_keys(PrivDir), + Config. + +hello(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +hello_from_server_first(Config) -> + DataDir = ?config(data_dir,Config), + ?NS:hello(1), + {ok,Client} = ct_netconfc:only_open(?DEFAULT_SSH_OPTS(DataDir)), + ct:sleep(500), + ?NS:expect(hello), + ?ok = ct_netconfc:hello(Client), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +hello_named(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(any_name,DataDir), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +hello_configured() -> + [{require, netconf1}]. +hello_configured(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_configured_success(netconf1,DataDir), + ?NS:expect_do_reply('close-session',close,ok), + {error, {no_such_name,netconf1}} = ct_netconfc:close_session(netconf1), + ?ok = ct_netconfc:close_session(Client), + ok. + +hello_configured_extraopts() -> + [{require, netconf1}]. +hello_configured_extraopts(Config) -> + DataDir = ?config(data_dir,Config), + %% Test that the cofiguration overwrites the ExtraOpts parameter + %% to ct_netconfc:open/2. + {ok,Client} = open_configured_success(netconf1,DataDir,[{password,"faulty"}]), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +hello_required() -> + [{require, my_named_connection, netconf1}]. +hello_required(Config) -> + DataDir = ?config(data_dir,Config), + {ok,_Client} = open_configured_success(my_named_connection,DataDir), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(my_named_connection), + ok. + +hello_required_exists() -> + [{require, my_named_connection, netconf1}]. +hello_required_exists(Config) -> + DataDir = ?config(data_dir,Config), + {ok,_Client1} = open_configured_success(my_named_connection,DataDir), + + %% Check that same name can not be used twice + {error,{connection_exists,_Client1}} = + ct_netconfc:open(my_named_connection,[{user_dir,DataDir}]), + + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(my_named_connection), + + %% Then check that it can be used again after the first is closed + {ok,_Client2} = open_configured_success(my_named_connection,DataDir), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(my_named_connection), + ok. + +hello_global_pwd(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir,[{user,"any-user"}, + {password,"global-xxx"}]), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +hello_no_session_id(Config) -> + DataDir = ?config(data_dir,Config), + ?NS:hello(no_session_id), + ?NS:expect(hello), + {error,{incorrect_hello,no_session_id_found}} = open(DataDir), + ok. + +hello_incomp_base_vsn(Config) -> + DataDir = ?config(data_dir,Config), + ?NS:hello(1,{base,"1.1"}), + ?NS:expect(hello), + {error,{incompatible_base_capability_vsn,"1.1"}} = open(DataDir), + ok. + +hello_no_base_cap(Config) -> + DataDir = ?config(data_dir,Config), + ?NS:hello(1,no_base), + ?NS:expect(hello), + {error,{incorrect_hello,no_base_capability_found}} = open(DataDir), + ok. + +hello_no_caps(Config) -> + DataDir = ?config(data_dir,Config), + ?NS:hello(1,no_caps), + ?NS:expect(hello), + {error,{incorrect_hello,capabilities_not_found}} = open(DataDir), + ok. + +no_server_hello(Config) -> + DataDir = ?config(data_dir,Config), + ?NS:expect(hello), + {error,{hello_session_failed,timeout}} = open(DataDir,[{timeout,2000}]), + ok. + +no_client_hello(Config) -> + DataDir = ?config(data_dir,Config), + ?NS:hello(1), + {ok,Client} = ct_netconfc:only_open(?DEFAULT_SSH_OPTS(DataDir)), + + %% Allow server hello to arrive + ct:sleep(500), + + %% Tell server to receive a get request and then die without + %% replying since no hello has been received. (is this correct + %% behavoiur??) + ?NS:expect_do(get,close), + {error,closed} = ct_netconfc:get(Client,whatever), + ok. + +get_session_id(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + + 1 = ct_netconfc:get_session_id(Client), + + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +get_capabilities(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + + Caps = ct_netconfc:get_capabilities(Client), + BaseCap = ?NETCONF_BASE_CAP ++ ?NETCONF_BASE_CAP_VSN, + [BaseCap,"urn:ietf:params:netconf:capability:writable-running:1.0" |_] = Caps, + + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +faulty_user(Config) -> + DataDir = ?config(data_dir,Config), + {error,{ssh,could_not_connect_to_server, + "Unable to connect using the available authentication methods"}} = + open(DataDir,[{user,"yyy"}]), + ok. + +faulty_passwd(Config) -> + DataDir = ?config(data_dir,Config), + {error,{ssh,could_not_connect_to_server, + "Unable to connect using the available authentication methods"}} = + open(DataDir,[{password,"yyy"}]), + ok. + +faulty_port(Config) -> + DataDir = ?config(data_dir,Config), + {error,{ssh,could_not_connect_to_server,econnrefused}} = + open(DataDir,[{port,2062}]), + ok. + +no_host(Config) -> + DataDir = ?config(data_dir,Config), + Opts = lists:keydelete(ssh,1,?DEFAULT_SSH_OPTS(DataDir)), + {error,no_host_address} = ct_netconfc:open(Opts), + ok. + +no_port(Config) -> + DataDir = ?config(data_dir,Config), + Opts = lists:keydelete(port,1,?DEFAULT_SSH_OPTS(DataDir)), + {error,no_port} = ct_netconfc:open(Opts), + ok. + +invalid_opt(Config) -> + DataDir = ?config(data_dir,Config), + Opts1 = ?DEFAULT_SSH_OPTS(DataDir) ++ [{timeout,invalidvalue}], + {error,{invalid_option,{timeout,invalidvalue}}} = ct_netconfc:open(Opts1), + Opts2 = ?DEFAULT_SSH_OPTS(DataDir) ++ [{some_other_opt,true}], + {error,{invalid_option,{some_other_opt,true}}} = ct_netconfc:open(Opts2), + ok. + +get(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + Data = [{server,[{xmlns,"myns"}],[{name,[],["myserver"]}]}], + ?NS:expect_reply('get',{data,Data}), + {ok,Data} = ct_netconfc:get(Client,{server,[{xmlns,"myns"}],[]}), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +get_xpath(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + Data = [{server,[{xmlns,"myns"}],[{name,[],["myserver"]}]}], + ?NS:expect_reply({'get',xpath},{data,Data}), + {ok,Data} = ct_netconfc:get(Client,{xpath,"/server"}), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +get_config(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + Data = [{server,[{xmlns,"myns"}],[{name,[],["myserver"]}]}], + ?NS:expect_reply('get-config',{data,Data}), + {ok,Data} = ct_netconfc:get_config(Client,running, + {server,[{xmlns,"myns"}],[]}), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +get_config_xpath(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + Data = [{server,[{xmlns,"myns"}],[{name,[],["myserver"]}]}], + ?NS:expect_reply({'get-config',xpath},{data,Data}), + {ok,Data} = ct_netconfc:get_config(Client,running,{xpath,"/server"}), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +edit_config(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + ?NS:expect_reply('edit-config',ok), + ?ok = ct_netconfc:edit_config(Client,running, + {server,[{xmlns,"myns"}], + [{name,["myserver"]}]}), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +copy_config(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + ?NS:expect_reply('copy-config',ok), + ?ok = ct_netconfc:copy_config(Client,startup,running), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +delete_config(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + ?NS:expect_reply('delete-config',ok), + ?ok = ct_netconfc:delete_config(Client,startup), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +lock(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + ?NS:expect_reply('lock',ok), + ?ok = ct_netconfc:lock(Client,running), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +unlock(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + ?NS:expect_reply('unlock',ok), + ?ok = ct_netconfc:unlock(Client,running), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +kill_session(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + + ?NS:hello(2), + ?NS:expect(hello), + {ok,_OtherClient} = open(DataDir), + + ?NS:expect_do_reply('kill-session',{kill,2},ok), + ?ok = ct_netconfc:kill_session(Client,2), + + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + + ok. + +get_no_such_client(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + case ct_netconfc:get(Client,{server,[{xmlns,"myns"}],[]}) of + {error,no_such_client} -> + ok; + {error,closed} -> + %% Means that the Client process was not terminated before the call. + %% Give it one more go. + {error,no_such_client} = + ct_netconfc:get(Client,{server,[{xmlns,"myns"}],[]}) + end, + ok. + +action(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + Data = [{myactionreturn,[{xmlns,"myns"}],["value"]}], + ?NS:expect_reply(action,{data,Data}), + {ok,Data} = ct_netconfc:action(Client,{myaction,[{xmlns,"myns"}],[]}), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +send_any_rpc(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + Data = [{server,[{xmlns,"myns"}],[{name,[],["myserver"]}]}], + GetConf = {'get-config', + [{source,["running"]}, + {filter,[{type,"subtree"}], + [{server,[{xmlns,"myns"}],[]}]}]}, + ?NS:expect_reply('get-config',{data,Data}), + [{data,?NETCONF_NAMESPACE_ATTR,Data}] = ct_netconfc:send_rpc(Client,GetConf), + + EditConf = {'edit-config', + [{target,["running"]}, + {config,[{server,[{xmlns,"myns"}], + [{name,["myserver"]}]}]}]}, + ?NS:expect_reply('edit-config',ok), + [{ok,?NETCONF_NAMESPACE_ATTR,[]}] = ct_netconfc:send_rpc(Client,EditConf), + + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +send_any(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + + %% Correct get-config rpc + Data = [{server,[{xmlns,"myns"}],[{name,[],["myserver"]}]}], + RpcAttr1 = ?NETCONF_NAMESPACE_ATTR ++ [{'message-id',"1"}], + RpcGetConf = {rpc,RpcAttr1, + [{'get-config', + [{source,["running"]}, + {filter,[{type,"subtree"}], + [{server,[{xmlns,"myns"}],[]}]}]}]}, + ?NS:expect_reply('get-config',{data,Data}), + {'rpc-reply',RpcAttr1,[{data,_,Data}]} = ct_netconfc:send(Client,RpcGetConf), + + %% Correct edit-config rpc + RpcAttr2 = ?NETCONF_NAMESPACE_ATTR ++ [{'message-id',"2"}], + RpcEditConf = {rpc,RpcAttr2, + [{'edit-config', + [{target,["running"]}, + {config,[{server,[{xmlns,"myns"}], + [{name,["myserver"]}]}]}]}]}, + ?NS:expect_reply('edit-config',ok), + {'rpc-reply',RpcAttr2,[{ok,_,[]}]} = ct_netconfc:send(Client,RpcEditConf), + + %% Send any data + ?NS:expect_reply(any,{ok,[],[]}), + {ok,_,[]} = ct_netconfc:send(Client,{any,[],[]}), + + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +hide_password(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + Password = "my_very_secret_password", + Data = [{passwords,[{xmlns,"myns"}], + [{password,[{xmlns,"pwdns"}],[Password]}, + {password,[],[Password]}]}], + ?NS:expect_reply('get',{data,Data}), + ct:capture_start(), % in case of html logging + {ok,Data} = ct_netconfc:get(Client,{passwords,[{xmlns,"myns"}],[]}), + ct:capture_stop(), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + + Log = filename:join(?config(priv_dir,Config),"hide_password-netconf.txt"), + + Text = + case file:read_file(Log) of + {ok,Bin} -> + Bin; + _NoLog -> + %% Assume html logging + list_to_binary(ct:capture_get()) + end, + + nomatch = binary:match(Text,list_to_binary(Password)), + + ok. + +not_proper_xml(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + NS = list_to_binary(?NETCONF_NAMESPACE), + NotProper = <<"<rpc-reply message-id=\"1\" xmlns=\"", + NS/binary,"\"><data></rpc-reply>">>, + ?NS:expect_reply('get',NotProper), + {error,{failed_to_parse_received_data,_}} = + ct_netconfc:get(Client,{server,[{xmlns,"myns"}],[]}), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +prefixed_namespace(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + NS = list_to_binary(?NETCONF_NAMESPACE), + + %% Test that data element can be properly decoded and that + %% prefixed namespace attributes (exepct the netconf namespace) + %% are forwarded to the content of the data element - i.e. that + %% the xmlns:my is forwarded from the rpc-reply element to the + %% server element below. + Data = <<"<nc:rpc-reply message-id=\"1\" xmlns:nc=\"", + NS/binary,"\" xmlns:my=\"myns\"><nc:data><my:server>", + "<my:name my:lang=\"en\">myserver</my:name></my:server>" + "</nc:data></nc:rpc-reply>">>, + ?NS:expect_reply('get',Data), + {ok,[{'my:server',[{'xmlns:my',"myns"}], + [{'my:name',[{'my:lang',"en"}],["myserver"]}]}]} = + ct_netconfc:get(Client,{server,[{xmlns,"myns"}],[]}), + + Ok = <<"<nc:rpc-reply message-id=\"2\" xmlns:nc=\"", + NS/binary,"\"><nc:ok/></nc:rpc-reply>">>, + ?NS:expect_reply('edit-config',Ok), + ?ok = ct_netconfc:edit_config(Client,running, + {server,[{xmlns,"myns"}], + [{name,["myserver"]}]}), + + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +%% Test that the client can parse data which is received in chunks, +%% i.e. when the complete rpc-reply is not contained in one single ssh +%% data message. +receive_chunked_data(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + + %% Construct the data to return from netconf server + Data = [{servers,[{xmlns,"myns"}], + [{server,[],[{name,[],["server0"]}]}, + {server,[],[{name,[],["server1"]}]}, + {server,[],[{name,[],["server2"]}]}, + {server,[],[{name,[],["server3"]}]}, + {server,[],[{name,[],["server4"]}]}, + {server,[],[{name,[],["server5"]}]}, + {server,[],[{name,[],["server6"]}]}, + {server,[],[{name,[],["server7"]}]}, + {server,[],[{name,[],["server8"]}]}, + {server,[],[{name,[],["server9"]}]}] + }], + Rpc = {'rpc-reply',?NETCONF_NAMESPACE_ATTR ++ [{'message-id',"1"}], + [{data,Data}]}, + Xml = list_to_binary(xmerl:export_simple_element(Rpc,xmerl_xml)), + Netconf = + <<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", + Xml/binary,"\n",?END_TAG/binary>>, + + %% Split the data in some chunks + PartLength = size(Netconf) div 3, + <<Part1:PartLength/binary,Part2:PartLength/binary,Part3:PartLength/binary, + Part4/binary>> = Netconf, + + %% Spawn a process which will wait a bit for the client to send + %% the request (below), then order the server to the chunks of the + %% rpc-reply one by one. + spawn(fun() -> timer:sleep(500),?NS:hupp(send,Part1), + timer:sleep(100),?NS:hupp(send,Part2), + timer:sleep(100),?NS:hupp(send,Part3), + timer:sleep(100),?NS:hupp(send,Part4) + end), + + %% Order server to expect a get - then the process above will make + %% sure the rpc-reply is sent. + ?NS:expect('get'), + {ok,Data} = ct_netconfc:get(Client,{server,[{xmlns,"myns"}],[]}), + + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +%% Same as receive_chunked_data, but timeout waiting for last part. +timeout_receive_chunked_data(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + + %% Construct the data to return from netconf server + Data = [{servers,[{xmlns,"myns"}], + [{server,[],[{name,[],["server0"]}]}, + {server,[],[{name,[],["server1"]}]}, + {server,[],[{name,[],["server2"]}]}, + {server,[],[{name,[],["server3"]}]}, + {server,[],[{name,[],["server4"]}]}, + {server,[],[{name,[],["server5"]}]}, + {server,[],[{name,[],["server6"]}]}, + {server,[],[{name,[],["server7"]}]}, + {server,[],[{name,[],["server8"]}]}, + {server,[],[{name,[],["server9"]}]}] + }], + Rpc = {'rpc-reply',?NETCONF_NAMESPACE_ATTR ++ [{'message-id',"1"}], + [{data,Data}]}, + Xml = list_to_binary(xmerl:export_simple_element(Rpc,xmerl_xml)), + Netconf = + <<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", + Xml/binary,"\n",?END_TAG/binary>>, + + %% Split the data in some chunks + PartLength = size(Netconf) div 3, + <<Part1:PartLength/binary,Part2:PartLength/binary,_Part3:PartLength/binary, + _Part4/binary>> = Netconf, + + %% Spawn a process which will wait a bit for the client to send + %% the request (below), then order the server to the chunks of the + %% rpc-reply one by one. + spawn(fun() -> timer:sleep(500),?NS:hupp(send,Part1), + timer:sleep(100),?NS:hupp(send,Part2) + end), + + %% Order server to expect a get - then the process above will make + %% sure the rpc-reply is sent - but only a part of it - then timeout. + ?NS:expect('get'), + {error,timeout} = ct_netconfc:get(Client,{server,[{xmlns,"myns"}],[]},2000), + + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +%% Same as receive_chunked_data, but timeout waiting for last part. +close_while_waiting_for_chunked_data(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + + %% Construct the data to return from netconf server + Data = [{servers,[{xmlns,"myns"}], + [{server,[],[{name,[],["server0"]}]}, + {server,[],[{name,[],["server1"]}]}, + {server,[],[{name,[],["server2"]}]}, + {server,[],[{name,[],["server3"]}]}, + {server,[],[{name,[],["server4"]}]}, + {server,[],[{name,[],["server5"]}]}, + {server,[],[{name,[],["server6"]}]}, + {server,[],[{name,[],["server7"]}]}, + {server,[],[{name,[],["server8"]}]}, + {server,[],[{name,[],["server9"]}]}] + }], + Rpc = {'rpc-reply',?NETCONF_NAMESPACE_ATTR ++ [{'message-id',"1"}], + [{data,Data}]}, + Xml = list_to_binary(xmerl:export_simple_element(Rpc,xmerl_xml)), + Netconf = + <<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", + Xml/binary,"\n",?END_TAG/binary>>, + + %% Split the data in some chunks + PartLength = size(Netconf) div 3, + <<Part1:PartLength/binary,Part2:PartLength/binary,_Part3:PartLength/binary, + _Part4/binary>> = Netconf, + + %% Spawn a process which will wait a bit for the client to send + %% the request (below), then order the server to the chunks of the + %% rpc-reply one by one. + spawn(fun() -> timer:sleep(500),?NS:hupp(send,Part1), + timer:sleep(100),?NS:hupp(send,Part2), + timer:sleep(100),?NS:hupp(kill) + end), + + %% Order server to expect a get - then the process above will make + %% sure the rpc-reply is sent - but only a part of it - then close. + ?NS:expect('get'), + {error,closed} = ct_netconfc:get(Client,{server,[{xmlns,"myns"}],[]},2000), + ok. + +connection_crash(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + + %% Test that if the test survives killing the connection + %% process. Earlier this caused ct_util_server to terminate, and + %% this aborting the complete test run. + spawn(fun() -> timer:sleep(500),exit(Client,kill) end), + ?NS:expect(get), + {error,{closed,killed}}=ct_netconfc:get(Client,{server,[{xmlns,"myns"}],[]}), + ok. + +get_event_streams(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + StreamNames = ["NETCONF","stream1","stream2"], + Streams = [{N,[{description,"descr of " ++ N}]} || N <- StreamNames], + StreamsXml = [{stream,[{name,[N]}|[{Tag,[Value]} || {Tag,Value} <- Data]]} + || {N,Data} <- Streams], + ReplyData = [{netconf,?NETMOD_NOTIF_NAMESPACE_ATTR,[{streams,StreamsXml}]}], + ?NS:expect_reply('get',{data,ReplyData}), + {ok,Streams} = ct_netconfc:get_event_streams(Client,StreamNames), + + ?NS:expect_reply('get',{data,ReplyData}), + {ok,Streams} = ct_netconfc:get_event_streams(Client,StreamNames,5000), + + ?NS:expect('get'), + {error,timeout} = ct_netconfc:get_event_streams(Client,100), + + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + ok. + +create_subscription(Config) -> + DataDir = ?config(data_dir,Config), + + %% All defaults + {ok,Client1} = open_success(DataDir), + ?NS:expect_reply({'create-subscription',[stream]},ok), + ?ok = ct_netconfc:create_subscription(Client1), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client1), + + %% All defaults with timeout + {ok,Client1a} = open_success(DataDir), + ?NS:expect_reply({'create-subscription',[stream]},ok), + ?ok = ct_netconfc:create_subscription(Client1a,5000), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client1a), + + %% All defaults timing out + {ok,Client1b} = open_success(DataDir), + ?NS:expect({'create-subscription',[stream]}), + {error,timeout} = ct_netconfc:create_subscription(Client1b,100), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client1b), + + %% Stream + {ok,Client2} = open_success(DataDir), + ?NS:expect_reply({'create-subscription',[stream]},ok), + Stream = "some_stream", + ?ok = ct_netconfc:create_subscription(Client2,Stream), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client2), + + %% Filter + {ok,Client3} = open_success(DataDir), + ?NS:expect_reply({'create-subscription',[stream,filter]},ok), + Filter = {notification,?NETMOD_NOTIF_NAMESPACE_ATTR, + [eventTime]}, + ?ok = ct_netconfc:create_subscription(Client3,Filter), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client3), + + %% Filter with timeout + {ok,Client3a} = open_success(DataDir), + ?NS:expect_reply({'create-subscription',[stream,filter]},ok), + ?ok = ct_netconfc:create_subscription(Client3a,Filter,5000), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client3a), + + %% Filter timing out + {ok,Client3b} = open_success(DataDir), + ?NS:expect({'create-subscription',[stream,filter]}), + {error,timeout}=ct_netconfc:create_subscription(Client3b,Filter,100), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client3b), + + %% Stream and filter + {ok,Client4} = open_success(DataDir), + ?NS:expect_reply({'create-subscription',[stream,filter]},ok), + ?ok = ct_netconfc:create_subscription(Client4,Stream,Filter), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client4), + + %% Start/stop time + {ok,Client5} = open_success(DataDir), + ?NS:expect_reply({'create-subscription',[stream,startTime,stopTime]},ok), + StartTime = xs_datetime({D,{H,M,S}}= calendar:local_time()), + StopTime = xs_datetime({D,{H+2,M,S}}), + ?ok = ct_netconfc:create_subscription(Client5,StartTime,StopTime), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client5), + + %% Start/stop time with timeout + {ok,Client5a} = open_success(DataDir), + ?NS:expect_reply({'create-subscription',[stream,startTime,stopTime]},ok), + ?ok = ct_netconfc:create_subscription(Client5a,StartTime,StopTime,5000), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client5a), + + %% Start/stop time timing out + {ok,Client5b} = open_success(DataDir), + ?NS:expect({'create-subscription',[stream,startTime,stopTime]}), + {error,timeout} = + ct_netconfc:create_subscription(Client5b,StartTime,StopTime,100), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client5b), + + %% Stream and start/stop time + {ok,Client6} = open_success(DataDir), + ?NS:expect_reply({'create-subscription',[stream,startTime,stopTime]},ok), + ?ok = ct_netconfc:create_subscription(Client6,Stream,StartTime,StopTime), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client6), + + %% Filter and start/stop time + {ok,Client7} = open_success(DataDir), + ?NS:expect_reply({'create-subscription',[stream,filter,startTime,stopTime]}, + ok), + ?ok = ct_netconfc:create_subscription(Client7,Filter, + StartTime,StopTime), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client7), + + %% Stream, filter and start/stop time + {ok,Client8} = open_success(DataDir), + ?NS:expect_reply({'create-subscription',[stream,filter,startTime,stopTime]}, + ok), + ?ok = ct_netconfc:create_subscription(Client8,Stream,Filter, + StartTime,StopTime), + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client8), + + ok. + +receive_event(Config) -> + DataDir = ?config(data_dir,Config), + {ok,Client} = open_success(DataDir), + ?NS:expect_reply({'create-subscription',[stream]},ok), + ?ok = ct_netconfc:create_subscription(Client), + + ?NS:hupp(send_event), + + receive + %% Matching ?NS:make_msg(event) + {notification,?NETCONF_NOTIF_NAMESPACE_ATTR, + [{eventTime,[],[_Time]}, + {event,[{xmlns,"http://my.namespaces.com/event"}], + [{severity,_,_}, + {description,_,_}]}]} -> + ok; + Other -> + ct:fail({got_unexpected_while_waiting_for_event, Other}) + after 3000 -> + ct:fail(timeout_waiting_for_event) + end, + + ?NS:expect_do_reply('close-session',close,ok), + ?ok = ct_netconfc:close_session(Client), + + ok. + +%%%----------------------------------------------------------------- + +break(_Config) -> + test_server:break("break test case"). + +br() -> + test_server:break(""). + +%%%----------------------------------------------------------------- +%% Open a netconf session which is not specified in a config file +open_success(Dir) -> + open_success(Dir,[]). + +%% Open a netconf session which is not specified in a config file, and +%% give som extra options in addition to the test defaults. +open_success(Dir,ExtraOpts) when is_list(Dir), is_list(ExtraOpts) -> + ?NS:hello(1), % tell server to send hello with session id 1 + ?NS:expect(hello), % tell server to expect a hello message from client + open(Dir,ExtraOpts); + +%% Open a named netconf session which is not specified in a config file +open_success(KeyOrName,Dir) when is_atom(KeyOrName), is_list(Dir) -> + ?NS:hello(1), + ?NS:expect(hello), + ct_netconfc:open(KeyOrName,?DEFAULT_SSH_OPTS(Dir)). + +open(Dir) -> + open(Dir,[]). +open(Dir,ExtraOpts) -> + Opts = lists:ukeymerge(1,lists:keysort(1,ExtraOpts), + lists:keysort(1,?DEFAULT_SSH_OPTS(Dir))), + ct_netconfc:open(Opts). + +%%%----------------------------------------------------------------- +%%% Open a netconf session which is specified in a config file +%%% KeyOrName is the config key (server_id()) or name given in a +%%% require statement (target_name()). +open_configured_success(KeyOrName,Dir) when is_atom(KeyOrName) -> + open_configured_success(KeyOrName,Dir,[]). +open_configured_success(KeyOrName,Dir,ExtraOpts) when is_atom(KeyOrName) -> + ?NS:hello(1), + ?NS:expect(hello), + ct_netconfc:open(KeyOrName,[{user_dir,Dir}|ExtraOpts]). + +%%%----------------------------------------------------------------- +%%% Convert erlang datetime to the simplest variant of XML dateTime +xs_datetime({{Y,M,D},{H,Mi,S}}) -> + lists:flatten( + io_lib:format("~p-~s-~sT~s:~s:~s",[Y,pad(M),pad(D),pad(H),pad(Mi),pad(S)])). + +pad(I) when I<10 -> + "0"++integer_to_list(I); +pad(I) -> + integer_to_list(I). + + +%%%----------------------------------------------------------------- +%%% BEGIN SSH key management +%% copy private keys to given dir from ~/.ssh +get_id_keys(Config) -> + DstDir = ?config(priv_dir, Config), + SrcDir = filename:join(os:getenv("HOME"), ".ssh"), + RsaOk = copyfile(SrcDir, DstDir, "id_rsa"), + DsaOk = copyfile(SrcDir, DstDir, "id_dsa"), + case {RsaOk, DsaOk} of + {{ok, _}, {ok, _}} -> {ok, both}; + {{ok, _}, _} -> {ok, rsa}; + {_, {ok, _}} -> {ok, dsa}; + {Error, _} -> Error + end. + +%% Remove later on. Use make_dsa_files instead. +remove_id_keys(Config) -> + Dir = ?config(priv_dir, Config), + file:delete(filename:join(Dir, "id_rsa")), + file:delete(filename:join(Dir, "id_dsa")). + + +make_dsa_files(Config) -> + make_dsa_files(Config, rfc4716_public_key). +make_dsa_files(Config, Type) -> + {DSA, EncodedKey} = gen_dsa(128, 20), + PKey = DSA#'DSAPrivateKey'.y, + P = DSA#'DSAPrivateKey'.p, + Q = DSA#'DSAPrivateKey'.q, + G = DSA#'DSAPrivateKey'.g, + Dss = #'Dss-Parms'{p=P, q=Q, g=G}, + {ok, Hostname} = inet:gethostname(), + {ok, {A, B, C, D}} = inet:getaddr(Hostname, inet), + IP = lists:concat([A, ".", B, ".", C, ".", D]), + Attributes = [], % Could be [{comment,"user@" ++ Hostname}], + HostNames = [{hostnames,[IP, IP]}], + PublicKey = [{{PKey, Dss}, Attributes}], + KnownHosts = [{{PKey, Dss}, HostNames}], + + KnownHostsEnc = public_key:ssh_encode(KnownHosts, known_hosts), + KnownHosts = public_key:ssh_decode(KnownHostsEnc, known_hosts), + + PublicKeyEnc = public_key:ssh_encode(PublicKey, Type), + + SystemTmpDir = ?config(data_dir, Config), + filelib:ensure_dir(SystemTmpDir), + file:make_dir(SystemTmpDir), + + DSAFile = filename:join(SystemTmpDir, "ssh_host_dsa_key.pub"), + file:delete(DSAFile), + + DSAPrivateFile = filename:join(SystemTmpDir, "ssh_host_dsa_key"), + file:delete(DSAPrivateFile), + + KHFile = filename:join(SystemTmpDir, "known_hosts"), + file:delete(KHFile), + + PemBin = public_key:pem_encode([EncodedKey]), + + file:write_file(DSAFile, PublicKeyEnc), + file:write_file(KHFile, KnownHostsEnc), + file:write_file(DSAPrivateFile, PemBin), + ok. + +%%-------------------------------------------------------------------- +%% Creates a dsa key (OBS: for testing only) +%% the sizes are in bytes +%% gen_dsa(::integer()) -> {::atom(), ::binary(), ::opaque()} +%%-------------------------------------------------------------------- +gen_dsa(LSize,NSize) when is_integer(LSize), is_integer(NSize) -> + Key = gen_dsa2(LSize, NSize), + {Key, encode_key(Key)}. + +encode_key(Key = #'RSAPrivateKey'{}) -> + {ok, Der} = 'OTP-PUB-KEY':encode('RSAPrivateKey', Key), + {'RSAPrivateKey', list_to_binary(Der), not_encrypted}; +encode_key(Key = #'DSAPrivateKey'{}) -> + {ok, Der} = 'OTP-PUB-KEY':encode('DSAPrivateKey', Key), + {'DSAPrivateKey', list_to_binary(Der), not_encrypted}. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% DSA key generation (OBS: for testing only) +%% See http://en.wikipedia.org/wiki/Digital_Signature_Algorithm +%% and the fips_186-3.pdf +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +gen_dsa2(LSize, NSize) -> + Q = prime(NSize), %% Choose N-bit prime Q + X0 = prime(LSize), + P0 = prime((LSize div 2) +1), + + %% Choose L-bit prime modulus P such that p-1 is a multiple of q. + case dsa_search(X0 div (2*Q*P0), P0, Q, 1000) of + error -> + gen_dsa2(LSize, NSize); + P -> + G = crypto:mod_exp(2, (P-1) div Q, P), % Choose G a number whose multiplicative order modulo p is q. + %% such that This may be done by setting g = h^(p-1)/q mod p, commonly h=2 is used. + + X = prime(20), %% Choose x by some random method, where 0 < x < q. + Y = crypto:mod_exp(G, X, P), %% Calculate y = g^x mod p. + + #'DSAPrivateKey'{version=0, p=P, q=Q, g=G, y=Y, x=X} + end. + +%% See fips_186-3.pdf +dsa_search(T, P0, Q, Iter) when Iter > 0 -> + P = 2*T*Q*P0 + 1, + case is_prime(crypto:mpint(P), 50) of + true -> P; + false -> dsa_search(T+1, P0, Q, Iter-1) + end; +dsa_search(_,_,_,_) -> + error. + + +%%%%%%% Crypto Math %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +prime(ByteSize) -> + Rand = odd_rand(ByteSize), + crypto:erlint(prime_odd(Rand, 0)). + +prime_odd(Rand, N) -> + case is_prime(Rand, 50) of + true -> + Rand; + false -> + NotPrime = crypto:erlint(Rand), + prime_odd(crypto:mpint(NotPrime+2), N+1) + end. + +%% see http://en.wikipedia.org/wiki/Fermat_primality_test +is_prime(_, 0) -> true; +is_prime(Candidate, Test) -> + CoPrime = odd_rand(<<0,0,0,4, 10000:32>>, Candidate), + case crypto:mod_exp(CoPrime, Candidate, Candidate) of + CoPrime -> is_prime(Candidate, Test-1); + _ -> false + end. + +odd_rand(Size) -> + Min = 1 bsl (Size*8-1), + Max = (1 bsl (Size*8))-1, + odd_rand(crypto:mpint(Min), crypto:mpint(Max)). + +odd_rand(Min,Max) -> + Rand = <<Sz:32, _/binary>> = crypto:rand_uniform(Min,Max), + BitSkip = (Sz+4)*8-1, + case Rand of + Odd = <<_:BitSkip, 1:1>> -> Odd; + Even = <<_:BitSkip, 0:1>> -> + crypto:mpint(crypto:erlint(Even)+1) + end. + +copyfile(SrcDir, DstDir, Fn) -> + file:copy(filename:join(SrcDir, Fn), + filename:join(DstDir, Fn)). + +%%% END SSH key management +%%%----------------------------------------------------------------- diff --git a/lib/common_test/test/ct_netconfc_SUITE_data/ns.erl b/lib/common_test/test/ct_netconfc_SUITE_data/ns.erl new file mode 100644 index 0000000000..665b0e556c --- /dev/null +++ b/lib/common_test/test/ct_netconfc_SUITE_data/ns.erl @@ -0,0 +1,506 @@ +%%-------------------------------------------------------------------- +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2012. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% +%%---------------------------------------------------------------------- +%% A netconf server used for testing of netconfc +-module(ns). + +%-compile(export_all). +-include_lib("common_test/src/ct_netconfc.hrl"). + + +%%%----------------------------------------------------------------- +%%% API +-export([start/1, + stop/1, + hello/1, + hello/2, + expect/1, + expect_reply/2, + expect_do/2, + expect_do_reply/3, + hupp/1, + hupp/2]). + +%%%----------------------------------------------------------------- +%%% ssh_channel callbacks +-export([init/1, + terminate/2, + handle_ssh_msg/2, + handle_msg/2]). + +%%%----------------------------------------------------------------- +%% Server specifications +-define(SERVER_DATA_NAMESPACE, "ClientTest"). +-define(CAPABILITIES,?CAPABILITIES_VSN("1.0")). +-define(CAPABILITIES_VSN(Vsn), + [ + ?NETCONF_BASE_CAP ++ Vsn, + "urn:ietf:params:netconf:capability:writable-running:1.0", + "urn:ietf:params:netconf:capability:candidate:1.0", + "urn:ietf:params:netconf:capability:confirmed-commit:1.0", + "urn:ietf:params:netconf:capability:rollback-on-error:1.0", + "urn:ietf:params:netconf:capability:startup:1.0", + "urn:ietf:params:netconf:capability:url:1.0", + "urn:ietf:params:netconf:capability:xpath:1.0", + "urn:ietf:params:netconf:capability:notification:1.0", + "urn:ietf:params:netconf:capability:interleave:1.0", + ?ACTION_NAMESPACE, + ?SERVER_DATA_NAMESPACE + ]). +-define(SSH_PORT, 2060). +-define(ssh_config(Dir),[{port, ?SSH_PORT}, + {interface, {127,0,0,1}}, + {system_dir, Dir}, + {user_dir, Dir}, + {user_passwords, [{"xxx","xxx"}]}, + {password, "global-xxx"}]). + +%% Some help for debugging +%-define(dbg(F,A),io:format(F,A)). +-define(dbg(F,A),ok). +-define(dbg_event(Event,Expect), + ?dbg("Event: ~p~nExpected: ~p~n",[Event,Expect])). + +%% State +-record(session, {cb, + connection, + buffer = <<>>, + session_id}). + + +%%%----------------------------------------------------------------- +%%% API + +%% Start the netconf server and use the given directory as system_dir +%% and user_dir +start(Dir) -> + spawn(fun() -> init_server(Dir) end). + +%% Stop the netconf server +stop(Pid) -> + Pid ! {stop,self()}, + receive stopped -> ok end. + +%% Set the session id for the hello message. +%% If this is not called prior to starting the session, no hello +%% message will be sent. +%% 'Stuff' indicates some special handling to e.g. provoke error cases +hello(SessionId) -> + hello(SessionId,undefined). +hello(SessionId,Stuff) -> + insert(hello,{SessionId,Stuff}). + +%% Tell server to expect the given message without doing any further +%% actions. To be called directly before sending a request. +expect(Expect) -> + expect_do_reply(Expect,undefined,undefined). + +%% Tell server to expect the given message and reply with the give +%% reply. To be called directly before sending a request. +expect_reply(Expect,Reply) -> + expect_do_reply(Expect,undefined,Reply). + +%% Tell server to expect the given message and perform an action. To +%% be called directly before sending a request. +expect_do(Expect,Do) -> + expect_do_reply(Expect,Do,undefined). + +%% Tell server to expect the given message, perform an action and +%% reply with the given reply. To be called directly before sending a +%% request. +expect_do_reply(Expect,Do,Reply) -> + add_expect({Expect,Do,Reply}). + +%% Hupp the server - i.e. tell it to do something - +%% e.g. hupp(send_event) will cause send_event(State) to be called on +%% the session channel process. +hupp(send_event) -> + hupp(send,[make_msg(event)]); +hupp(kill) -> + hupp(fun hupp_kill/1,[]). + +hupp(send,Data) -> + hupp(fun hupp_send/2,[Data]); +hupp(Fun,Args) when is_function(Fun) -> + [{_,Pid}] = lookup(channel_process), + Pid ! {hupp,Fun,Args}. + +%%%----------------------------------------------------------------- +%%% Main loop of the netconf server +init_server(Dir) -> + ets:new(ns_tab,[set,named_table,public]), + Config = ?ssh_config(Dir), + {_,Host} = lists:keyfind(interface, 1, Config), + {_,Port} = lists:keyfind(port, 1, Config), + Opts = lists:filter(fun({Key,_}) -> + lists:member(Key,[system_dir, + password, + user_passwords, + pwdfun]) + end, + Config), + {ok, Daemon} = + ssh:daemon(Host, Port, + [{subsystems,[{"netconf",{?MODULE,[]}}]} + |Opts]), + loop(Daemon). + +loop(Daemon) -> + receive + {stop,From} -> + ssh:stop_daemon(Daemon), + From ! stopped + end. + +%%---------------------------------------------------------------------- +%% Behaviour callback functions (ssh_channel) +%%---------------------------------------------------------------------- +init([]) -> + {ok, undefined}. + +terminate(_Reason, _State) -> + ok. + +handle_ssh_msg({ssh_cm,CM,{data, Ch, _Type = 0, Data}}, State) -> + %% erlang:display({self(),data,CM,Ch,State}), + data_for_channel(CM, Ch, Data, State); +handle_ssh_msg({ssh_cm,CM,{closed, Ch}}, State) -> + %% erlang:display({self(),closed,CM,Ch,State}), + stop_channel(CM, Ch, State); +handle_ssh_msg({ssh_cm,CM,{eof, Ch}}, State) -> + %% erlang:display({self(),eof,CM,Ch,State}), + data_for_channel(CM,Ch, <<>>, State). + + +handle_msg({'EXIT', _Pid, _Reason}, State) -> + {ok, State}; +handle_msg({ssh_channel_up,Ch,CM},undefined) -> + %% erlang:display({self(),up,CM,Ch}), + ConnRef = {CM,Ch}, + SessionId = maybe_hello(ConnRef), + insert(channel_process,self()), % used to hupp the server + {ok, #session{connection = ConnRef, + session_id = SessionId}}; +handle_msg({hupp,Fun,Args},State) -> + {ok,apply(Fun,Args ++ [State])}. + +data_for_channel(CM, Ch, Data, State) -> + try data(Data, State) of + {ok, NewState} -> + case erase(stop) of + true -> + stop_channel(CM, Ch, NewState); + _ -> + {ok, NewState} + end + catch + Class:Reason -> + Stacktrace = erlang:get_stacktrace(), + error_logger:error_report([{?MODULE, data_for_channel}, + {request, Data}, + {reason, {Class, Reason}}, + {stacktrace, Stacktrace}]), + stop_channel(CM, Ch, State) + end. + +data(Data, State = #session{connection = ConnRef, + buffer = Buffer}) -> + AllData = <<Buffer/binary,Data/binary>>, + case find_endtag(AllData) of + {ok,Msgs,Rest} -> + [check_expected(ConnRef,Msg) || Msg <- Msgs], + {ok,State#session{buffer=Rest}}; + need_more -> + {ok,State#session{buffer=AllData}} + end. + +stop_channel(CM, Ch, State) -> + ssh:close(CM), + {stop, Ch, State}. + + +%%%----------------------------------------------------------------- +%%% Functions to trigg via hupp/1: + +%% Send data spontaneously - e.g. an event +hupp_send(Data,State = #session{connection = ConnRef}) -> + send(ConnRef,Data), + State. +hupp_kill(State = #session{connection = ConnRef}) -> + kill(ConnRef), + State. + +%%%----------------------------------------------------------------- +%%% Internal functions + + +%%% Send ssh data to the client +send({CM,Ch},Data) -> + ssh_connection:send(CM, Ch, Data). + +%%% Kill ssh connection +kill({CM,_Ch}) -> + ssh:close(CM). + +add_expect(Add) -> + case lookup(expect) of + [] -> + insert(expect,[Add]); + [{expect,First}] -> + insert(expect,First ++ [Add]) + end, + ok. + +insert(Key,Value) -> + ets:insert(ns_tab,{Key,Value}). +lookup(Key) -> + ets:lookup(ns_tab,Key). + +maybe_hello(ConnRef) -> + case lookup(hello) of + [{hello,{SessionId,Stuff}}] -> + %% erlang:display({SessionId,Stuff}), + ets:delete(ns_tab,hello), + insert({session,SessionId},ConnRef), + reply(ConnRef,{hello,SessionId,Stuff}), + SessionId; + [] -> + undefined + end. + +find_endtag(Data) -> + case binary:split(Data,[?END_TAG],[global]) of + [Data] -> + need_more; + Msgs -> + {ok,lists:sublist(Msgs,length(Msgs)-1),lists:last(Msgs)} + end. + +check_expected(ConnRef,Msg) -> + case lookup(expect) of + [{expect,[{Expect,Do,Reply}|Rest]}] -> + insert(expect,Rest), + %% erlang:display({got,io_lib:format("~s",[Msg])}), + %% erlang:display({expected,Expect}), + match(Msg,Expect), + do(ConnRef, Do), + reply(ConnRef,Reply); + Expected -> + exit({error,{got_unexpected,Msg,Expected}}) + end. + +match(Msg,Expect) -> + ?dbg("Match: ~p~n",[Msg]), + {ok,ok,<<>>} = xmerl_sax_parser:stream(Msg,[{event_fun,fun event/3}, + {event_state,Expect}]). + +event(Event,_Loc,Expect) -> + ?dbg_event(Event,Expect), + event(Event,Expect). + +event(startDocument,Expect) -> match(Expect); +event({startElement,_,Name,_,Attrs},[{se,Name}|Match]) -> + msg_id(Name,Attrs), + Match; +event({startElement,_,Name,_,Attrs},[ignore,{se,Name}|Match]) -> + msg_id(Name,Attrs), + Match; +event({startElement,_,Name,_,Attrs},[{se,Name,As}|Match]) -> + msg_id(Name,Attrs), + match_attrs(Name,As,Attrs), + Match; +event({startElement,_,Name,_,Attrs},[ignore,{se,Name,As}|Match]) -> + msg_id(Name,Attrs), + match_attrs(Name,As,Attrs), + Match; +event({startPrefixMapping,_,Ns},[{ns,Ns}|Match]) -> Match; +event({startPrefixMapping,_,Ns},[ignore,{ns,Ns}|Match]) -> Match; +event({endPrefixMapping,_},Match) -> Match; +event({endElement,_,Name,_},[{ee,Name}|Match]) -> Match; +event({endElement,_,Name,_},[ignore,{ee,Name}|Match]) -> Match; +event(endDocument,Match) when Match==[]; Match==[ignore] -> ok; +event(_,[ignore|_]=Match) -> Match; +event(Event,Match) -> throw({nomatch,{Event,Match}}). + +msg_id("rpc",Attrs) -> + case lists:keyfind("message-id",3,Attrs) of + {_,_,_,Str} -> put(msg_id,Str); + false -> erase(msg_id) + end; +msg_id(_,_) -> + ok. + +match_attrs(Name,[{Key,Value}|As],Attrs) -> + case lists:keyfind(atom_to_list(Key),3,Attrs) of + {_,_,_,Value} -> match_attrs(Name,As,Attrs); + false -> throw({missing_attr,Key,Name,Attrs}); + _ -> throw({faulty_attr_value,Key,Name,Attrs}) + end; +match_attrs(_,[],_) -> + ok. + +do(ConnRef, close) -> + ets:match_delete(ns_tab,{{session,'_'},ConnRef}), + put(stop,true); +do(_ConnRef, {kill,SessionId}) -> + case lookup({session,SessionId}) of + [{_,Owner}] -> + ets:delete(ns_tab,{session,SessionId}), + kill(Owner); + _ -> + exit({no_session_to_kill,SessionId}) + end; +do(_, undefined) -> + ok. + +reply(_,undefined) -> + ?dbg("no reply~n",[]), + ok; +reply(ConnRef,Reply) -> + ?dbg("Reply: ~p~n",[Reply]), + send(ConnRef, make_msg(Reply)). + +from_simple(Simple) -> + list_to_binary(xmerl:export_simple_element(Simple,xmerl_xml)). + +xml(Content) -> + <<"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", + Content/binary,"\n",?END_TAG/binary>>. + +rpc_reply(Content) when is_binary(Content) -> + MsgId = case erase(msg_id) of + undefined -> <<>>; + Id -> list_to_binary([" message-id=\"",Id,"\""]) + end, + <<"<rpc-reply xmlns=\"",?NETCONF_NAMESPACE,"\"",MsgId/binary,">\n", + Content/binary,"\n</rpc-reply>">>; +rpc_reply(Content) -> + rpc_reply(list_to_binary(Content)). + +session_id(no_session_id) -> + <<>>; +session_id(SessionId0) -> + SessionId = list_to_binary(integer_to_list(SessionId0)), + <<"<session-id>",SessionId/binary,"</session-id>\n">>. + +capabilities(undefined) -> + CapsXml = list_to_binary([["<capability>",C,"</capability>\n"] + || C <- ?CAPABILITIES]), + <<"<capabilities>\n",CapsXml/binary,"</capabilities>\n">>; +capabilities({base,Vsn}) -> + CapsXml = list_to_binary([["<capability>",C,"</capability>\n"] + || C <- ?CAPABILITIES_VSN(Vsn)]), + <<"<capabilities>\n",CapsXml/binary,"</capabilities>\n">>; +capabilities(no_base) -> + [_|Caps] = ?CAPABILITIES, + CapsXml = list_to_binary([["<capability>",C,"</capability>\n"] || C <- Caps]), + <<"<capabilities>\n",CapsXml/binary,"</capabilities>\n">>; +capabilities(no_caps) -> + <<>>. + +%%%----------------------------------------------------------------- +%%% Match received netconf message from the client. Add a new clause +%%% for each new message to recognize. The clause argument shall match +%%% the Expect argument in expect/1, expect_reply/2 or +%%% expect_do_reply/3. +%%% +%%% match(term()) -> [Match]. +%%% Match = ignore | {se,Name} | {se,Name,Attrs} | {ee,Name} | {ns,Namespace} +%%% Name = string() +%%% Attrs = [{atom(),string()}] +%%% Namespace = string() +%%% +%%% 'se' means start element, 'ee' means end element - i.e. to match +%%% an XML element you need one 'se' entry and one 'ee' entry with the +%%% same name in the match list. +match(hello) -> + [ignore,{se,"hello"},ignore,{ee,"hello"},ignore]; +match('close-session') -> + [ignore,{se,"rpc"},{se,"close-session"}, + {ee,"close-session"},{ee,"rpc"},ignore]; +match('edit-config') -> + [ignore,{se,"rpc"},{se,"edit-config"},{se,"target"},ignore,{ee,"target"}, + {se,"config"},ignore,{ee,"config"},{ee,"edit-config"},{ee,"rpc"},ignore]; +match('get') -> + match({get,subtree}); +match({'get',FilterType}) -> + [ignore,{se,"rpc"},{se,"get"},{se,"filter",[{type,atom_to_list(FilterType)}]}, + ignore,{ee,"filter"},{ee,"get"},{ee,"rpc"},ignore]; +match('get-config') -> + match({'get-config',subtree}); +match({'get-config',FilterType}) -> + [ignore,{se,"rpc"},{se,"get-config"},{se,"source"},ignore,{ee,"source"}, + {se,"filter",[{type,atom_to_list(FilterType)}]},ignore,{ee,"filter"}, + {ee,"get-config"},{ee,"rpc"},ignore]; +match('copy-config') -> + [ignore,{se,"rpc"},{se,"copy-config"},{se,"target"},ignore,{ee,"target"}, + {se,"source"},ignore,{ee,"source"},{ee,"copy-config"},{ee,"rpc"},ignore]; +match('delete-config') -> + [ignore,{se,"rpc"},{se,"delete-config"},{se,"target"},ignore,{ee,"target"}, + {ee,"delete-config"},{ee,"rpc"},ignore]; +match('lock') -> + [ignore,{se,"rpc"},{se,"lock"},{se,"target"},ignore,{ee,"target"}, + {ee,"lock"},{ee,"rpc"},ignore]; +match('unlock') -> + [ignore,{se,"rpc"},{se,"unlock"},{se,"target"},ignore,{ee,"target"}, + {ee,"unlock"},{ee,"rpc"},ignore]; +match('kill-session') -> + [ignore,{se,"rpc"},{se,"kill-session"},{se,"session-id"},ignore, + {ee,"session-id"},{ee,"kill-session"},{ee,"rpc"},ignore]; +match(action) -> + [ignore,{se,"rpc"},{ns,?ACTION_NAMESPACE},{se,"action"},{se,"data"},ignore, + {ee,"data"},{ee,"action"},{ee,"rpc"},ignore]; +match({'create-subscription',Content}) -> + [ignore,{se,"rpc"},{ns,?NETCONF_NOTIF_NAMESPACE}, + {se,"create-subscription"}] ++ + lists:flatmap(fun(X) -> + [{se,atom_to_list(X)},ignore,{ee,atom_to_list(X)}] + end, Content) ++ + [{ee,"create-subscription"},{ee,"rpc"},ignore]; +match(any) -> + [ignore]. + + + +%%%----------------------------------------------------------------- +%%% Make message to send to the client. +%%% Add a new clause for each new message that shall be sent. The +%%% clause shall match the Reply argument in expect_reply/2 or +%%% expect_do_reply/3. +make_msg({hello,SessionId,Stuff}) -> + SessionIdXml = session_id(SessionId), + CapsXml = capabilities(Stuff), + xml(<<"<hello xmlns=\"",?NETCONF_NAMESPACE,"\">\n",CapsXml/binary, + SessionIdXml/binary,"</hello>">>); +make_msg(ok) -> + xml(rpc_reply("<ok/>")); +make_msg({data,Data}) -> + xml(rpc_reply(from_simple({data,Data}))); +make_msg(event) -> + xml(<<"<notification xmlns=\"",?NETCONF_NOTIF_NAMESPACE,"\">" + "<eventTime>2012-06-14T14:50:54+02:00</eventTime>" + "<event xmlns=\"http://my.namespaces.com/event\">" + "<severity>major</severity>" + "<description>Something terrible happened</description>" + "</event>" + "</notification>">>); +make_msg(Xml) when is_binary(Xml) -> + xml(Xml); +make_msg(Simple) when is_tuple(Simple) -> + xml(from_simple(Simple)). diff --git a/lib/common_test/test/ct_test_support.erl b/lib/common_test/test/ct_test_support.erl index 02246b5763..80cca4a1cc 100644 --- a/lib/common_test/test/ct_test_support.erl +++ b/lib/common_test/test/ct_test_support.erl @@ -35,6 +35,8 @@ verify_events/3, reformat/2, log_events/4, join_abs_dirs/2]). +-export([ct_test_halt/1]). + -include_lib("kernel/include/file.hrl"). %%%----------------------------------------------------------------- @@ -229,8 +231,9 @@ run(Opts, Config) when is_list(Opts) -> %% use ct interface test_server:format(Level, "~n[RUN #1] Calling ct:run_test(~p) on ~p~n", [Opts, CTNode]), - Result1 = rpc:call(CTNode, ct, run_test, [Opts]), - + CtRunTestResult = rpc:call(CTNode, ct, run_test, [Opts]), + test_server:format(Level, "~n[RUN #1] Got return value ~p~n", + [CtRunTestResult]), case rpc:call(CTNode, erlang, whereis, [ct_util_server]) of undefined -> ok; @@ -242,17 +245,36 @@ run(Opts, Config) when is_list(Opts) -> undefined = rpc:call(CTNode, erlang, whereis, [ct_util_server]) end, %% use run_test interface (simulated) - test_server:format(Level, "Saving start opts on ~p: ~p~n", [CTNode,Opts]), - rpc:call(CTNode, application, set_env, [common_test, run_test_start_opts, Opts]), - test_server:format(Level, "[RUN #2] Calling ct_run:script_start() on ~p~n", [CTNode]), - Result2 = rpc:call(CTNode, ct_run, script_start, []), - case {Result1,Result2} of - {ok,ok} -> + Opts1 = [{halt_with,{?MODULE,ct_test_halt}} | Opts], + test_server:format(Level, "Saving start opts on ~p: ~p~n", + [CTNode, Opts1]), + rpc:call(CTNode, application, set_env, + [common_test, run_test_start_opts, Opts1]), + test_server:format(Level, "[RUN #2] Calling ct_run:script_start() on ~p~n", + [CTNode]), + ExitStatus = rpc:call(CTNode, ct_run, script_start, []), + test_server:format(Level, "[RUN #2] Got exit status value ~p~n", + [ExitStatus]), + case {CtRunTestResult,ExitStatus} of + {{_Ok,Failed,{_UserSkipped,_AutoSkipped}},1} when Failed > 0 -> + ok; + {{_Ok,0,{_UserSkipped,AutoSkipped}},ExitStatus} when AutoSkipped > 0 -> + case proplists:get_value(exit_status, Opts1) of + ignore_config when ExitStatus == 1 -> + {error,{wrong_exit_status,ExitStatus}}; + _ -> + ok + end; + {{error,_}=Error,ExitStatus} -> + if ExitStatus /= 2 -> + {error,{wrong_exit_status,ExitStatus}}; + ExitStatus == 2 -> + Error + end; + {{_Ok,0,{_UserSkipped,_AutoSkipped}},0} -> ok; - {E,_} when E =/= ok -> - E; - {_,E} when E =/= ok -> - E + Unexpected -> + {error,{unexpected_return_value,Unexpected}} end. run(M, F, A, Config) -> @@ -272,6 +294,10 @@ run({M,F,A}, InitCalls, Config) -> [M, F, A, CTNode]), rpc:call(CTNode, M, F, A). +%% this is the last function that ct_run:script_start() calls, so the +%% return value here is what rpc:call/4 above returns +ct_test_halt(ExitStatus) -> + ExitStatus. %%%----------------------------------------------------------------- %%% wait_for_ct_stop/1 diff --git a/lib/common_test/test/ct_testspec_1_SUITE.erl b/lib/common_test/test/ct_testspec_1_SUITE.erl index b6dcf63fdf..693e8c6567 100644 --- a/lib/common_test/test/ct_testspec_1_SUITE.erl +++ b/lib/common_test/test/ct_testspec_1_SUITE.erl @@ -621,7 +621,9 @@ setup_and_execute(TCName, TestSpec, Config) -> ok = ct_test_support:run(Opts, Config), TestSpec1 = [{logdir,proplists:get_value(logdir,Opts)}, {label,proplists:get_value(label,TestTerms)} | TestSpec], - ok = ct_test_support:run(ct, run_testspec, [TestSpec1], Config), + {_Ok,_Failed,{_USkipped,_ASkipped}} = + ct_test_support:run(ct, run_testspec, [TestSpec1], Config), + Events = ct_test_support:get_events(ERPid, Config), ct_test_support:log_events(TCName, diff --git a/lib/common_test/test/ct_testspec_2_SUITE.erl b/lib/common_test/test/ct_testspec_2_SUITE.erl new file mode 100644 index 0000000000..93f3520f95 --- /dev/null +++ b/lib/common_test/test/ct_testspec_2_SUITE.erl @@ -0,0 +1,751 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2009-2011. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%%%------------------------------------------------------------------- +%%% File: ct_testspec_2_SUITE +%%% +%%% Description: +%%% Test test specifications +%%% +%%% The suites used for the test are located in the data directory. +%%%------------------------------------------------------------------- +-module(ct_testspec_2_SUITE). + +-compile(export_all). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("common_test/src/ct_util.hrl"). + +%%-------------------------------------------------------------------- +%% TEST SERVER CALLBACK FUNCTIONS +%%-------------------------------------------------------------------- + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + ok. + +init_per_testcase(_TestCase, Config) -> + Config. + +end_per_testcase(_TestCase, _Config) -> + ok. + +%% suite() -> [{ct_hooks,[ts_install_cth]}]. + +all() -> + [basic_compatible_no_nodes, + basic_compatible_nodes, + unknown_terms, + no_merging, + multiple_specs, + misc_config_terms, + define_names_1]. + + +%%-------------------------------------------------------------------- +%% VALID TEST SPEC TERMS (R15B02): +%% +%% {node,3} +%% {cover,2} +%% {cover,3} +%% {config,2} +%% {config,3} +%% {config,4} +%% {userconfig,2} +%% {userconfig,3} +%% {alias,3} +%% {merge_tests,2} +%% {logdir,2} +%% {logdir,3} +%% {logopts,2} +%% {logopts,3} +%% {basic_html,2} +%% {basic_html,3} +%% {label,2} +%% {label,3} +%% {event_handler,2} +%% {event_handler,3} +%% {event_handler,4} +%% {ct_hooks,2} +%% {ct_hooks,3} +%% {enable_builtin_hooks,2} +%% {noinput,2} +%% {multiply_timetraps,2} +%% {multiply_timetraps,3} +%% {scale_timetraps,2} +%% {scale_timetraps,3} +%% {include,2} +%% {include,3} +%% {auto_compile,2} +%% {auto_compile,3} +%% {stylesheet,2} +%% {stylesheet,3} +%% {suites,3} +%% {suites,4} +%% {groups,4} +%% {groups,5} +%% {groups,6} +%% {cases,4} +%% {cases,5} +%% {skip_suites,4} +%% {skip_suites,5} +%% {skip_groups,5} +%% {skip_groups,6} +%% {skip_groups,7} +%% {skip_cases,5} +%% {skip_cases,6} +%% {create_priv_dir,2} +%% +%%-------------------------------------------------------------------- + +%%-------------------------------------------------------------------- +%% TEST CASES +%%-------------------------------------------------------------------- + +%%%----------------------------------------------------------------- +%%% +basic_compatible_no_nodes(_Config) -> + + AliasDir1 = "../tests/to1", + AliasDir2 = "../tests/to2", + CfgDir1 = "../cfgs/to1/x.cfg", + CfgDir2 = ["../cfgs/to2/x.cfg","../cfgs/to2/y.cfg"], + LogDir = "../logs", + IncludeDir1 = "../../include", + IncludeDir2 = ["../tests/to1/include","../tests/to2/include"], + + Spec = + [ + {label,"basic_compatible_no_nodes"}, + {alias,to1,AliasDir1}, + {alias,to2,AliasDir2}, + {config,CfgDir1}, + {config,CfgDir2}, + {userconfig,{?MODULE,"cfg_str1"}}, + {userconfig,{?MODULE,"cfg_str2"}}, + {logdir,LogDir}, + {logopts,[no_nl]}, + {event_handler,evh1,[1]}, + {event_handler,[evh2,evh3],[[2,3]]}, + {ct_hooks,[{cth_mod1,[]}]}, + {ct_hooks,[{cth_mod2,[]}]}, + {multiply_timetraps,2}, + {include,IncludeDir1}, + {include,IncludeDir2}, + {suites,to1,[x_SUITE]}, + {groups,to1,y_SUITE,[g1,g2]}, + {cases,to1,y_SUITE,[tc1,tc2]}, + {skip_suites,to1,z_SUITE,"skipped"}, + {suites,to2,[x_SUITE,y_SUITE]}, + {skip_groups,to2,x_SUITE,[g1,g2],"skipped"}, + {skip_cases,to2,y_SUITE,[tc1,tc2],"skipped"} + ], + + {ok,SpecDir} = file:get_cwd(), + + ListResult = ct_testspec:collect_tests_from_list(Spec, false), + ct:pal("TESTSPEC RECORD FROM LIST:~n~p~n", [rec2proplist(ListResult)]), + SpecFile = ct_test_support:write_testspec(Spec,SpecDir, + "basic_compatible_no_nodes.spec"), + FileResult = ct_testspec:collect_tests_from_file([SpecFile], false), + ct:pal("TESTSPEC RECORD FROM FILE:~n~p~n", [rec2proplist(FileResult)]), + + Node = node(), + LogDirV = get_absdir(filename:join(SpecDir,"../logs")), + Alias1V = get_absdir(filename:join(SpecDir,AliasDir1)), + Alias2V = get_absdir(filename:join(SpecDir,AliasDir2)), + CFGs = [{Node,get_absdir(filename:join(SpecDir,CfgDir))} || + CfgDir <- [CfgDir1 | CfgDir2]], + Incls = [{Node,get_absdir(filename:join(SpecDir,IncludeDir))} || + IncludeDir <- [IncludeDir1 | IncludeDir2]], + + Verify = #testspec{spec_dir = SpecDir, + nodes = [{undefined,Node}], + init = [], + label = [{Node,"basic_compatible_no_nodes"}], + logdir = [{Node,LogDirV},"."], + logopts = [{Node,[no_nl]}], + basic_html = [], + cover = [], + config = CFGs, + userconfig = [{Node,{?MODULE,"cfg_str1"}}, + {Node,{?MODULE,"cfg_str2"}}], + event_handler = [{Node,evh1,[1]}, + {Node,evh2,[[2,3]]}, + {Node,evh3,[[2,3]]}], + ct_hooks = [{Node,{cth_mod1,[]}}, + {Node,{cth_mod2,[]}}], + enable_builtin_hooks = true, + noinput = false, + include = Incls, + auto_compile = [], + stylesheet = [], + multiply_timetraps = [{Node,2}], + scale_timetraps = [], + create_priv_dir = [], + alias = [{to1,Alias1V},{to2,Alias2V}], + tests = [{{Node,Alias1V}, + [{x_SUITE,[all]}, + {y_SUITE,[{g1,all},{g2,all},tc1,tc2]}, + {z_SUITE,[{all,{skip,"skipped"}}]}]}, + {{Node,Alias2V}, + [{x_SUITE,[all, + {{g1,all},{skip,"skipped"}}, + {{g2,all},{skip,"skipped"}}]}, + {y_SUITE,[all, + {tc1,{skip,"skipped"}}, + {tc2,{skip,"skipped"}}]}]}], + merge_tests = true}, + + verify_result(Verify,ListResult,FileResult). + +%%%----------------------------------------------------------------- +%%% +basic_compatible_nodes(_Config) -> + + Node1 = node1@host1, + Node2 = node2@host2, + TODir1 = "../tests/to1", + TODir2 = "../tests/to2", + CfgDir1 = "../cfgs/to1/x.cfg", + CfgDir2 = ["../cfgs/to2/x.cfg","../cfgs/to2/y.cfg"], + LogDir = "../logs", + MasterLogDir = "../master_logs", + IncludeDir1 = "../../include", + IncludeDir2 = ["../tests/to1/include","../tests/to2/include"], + + Spec = + [ + {node,n1,Node1}, + {node,n2,Node2}, + {init,[n1],[{node_start,[{callback_module,cbm}]}]}, + {init,n2,[{node_start,[]}]}, + {init,all_nodes,{eval,{mod,func,[]}}}, + {label,"basic_compatible_nodes"}, + {label,n1,basic_compatible_nodes_1}, + {config,n1,CfgDir1}, + {config,n2,CfgDir2}, + {userconfig,{?MODULE,"cfg_str1"}}, + {userconfig,{?MODULE,"cfg_str2"}}, + {logdir,all_nodes,LogDir}, + {logdir,master,MasterLogDir}, + {logopts,node2@host2,[no_nl]}, + {event_handler,master,evh1,[1]}, + {event_handler,[n1,n2],[evh2,evh3],[[2,3]]}, + {ct_hooks,all_nodes,[{cth_mod1,[]}]}, + {ct_hooks,[{cth_mod2,[]}]}, + {multiply_timetraps,node1@host1,2}, + {include,n1,IncludeDir1}, + {include,[n1,n2],IncludeDir2}, + {suites,n1,TODir1,[x_SUITE]}, + {groups,n1,TODir1,y_SUITE,[g1,g2]}, + {cases,n1,TODir1,y_SUITE,[tc1,tc2]}, + {skip_suites,n1,TODir1,z_SUITE,"skipped"}, + {suites,n2,TODir2,[x_SUITE,y_SUITE]}, + {skip_groups,n2,TODir2,x_SUITE,[g1,g2],"skipped"}, + {skip_cases,n2,TODir2,y_SUITE,[tc1,tc2],"skipped"} + ], + + {ok,SpecDir} = file:get_cwd(), + + ListResult = ct_testspec:collect_tests_from_list(Spec, false), + ct:pal("TESTSPEC RECORD FROM LIST:~n~p~n", [rec2proplist(ListResult)]), + SpecFile = ct_test_support:write_testspec(Spec,SpecDir, + "basic_compatible_nodes.spec"), + FileResult = ct_testspec:collect_tests_from_file([SpecFile], false), + ct:pal("TESTSPEC RECORD FROM FILE:~n~p~n", [rec2proplist(FileResult)]), + + Node = node(), + LogDirV = get_absdir(filename:join(SpecDir,"../logs")), + MasterLogDirV = get_absdir(filename:join(SpecDir,"../master_logs")), + TO1V = get_absdir(filename:join(SpecDir,TODir1)), + TO2V = get_absdir(filename:join(SpecDir,TODir2)), + CFGs = [{Node1,get_absdir(filename:join(SpecDir,CfgDir1))} | + [{Node2,get_absdir(filename:join(SpecDir,CfgDir))} || CfgDir <- CfgDir2]], + Incls = [{Node1,get_absdir(filename:join(SpecDir,IncludeDir1))} | + [{Node1,get_absdir(filename:join(SpecDir,IncludeDir))} || + IncludeDir <- IncludeDir2] ++ + [{Node2,get_absdir(filename:join(SpecDir,IncludeDir))} || + IncludeDir <- IncludeDir2]], + + Verify = #testspec{spec_dir = SpecDir, + nodes = [{undefined,Node},{n1,Node1},{n2,Node2}], + init = [{Node1,[{node_start,[{callback_module,cbm}]}, + {eval,[{mod,func,[]}]}]}, + {Node2,[{node_start,[{callback_module,ct_slave}]}, + {eval,[{mod,func,[]}]}]}, + {Node,[{node_start,[]}, + {eval,[{mod,func,[]}]}]}], + label = [{Node,"basic_compatible_nodes"}, + {Node2,"basic_compatible_nodes"}, + {Node1,basic_compatible_nodes_1}], + logdir = [{Node,LogDirV},{Node1,LogDirV},{Node2,LogDirV}, + {master,MasterLogDirV},"."], + logopts = [{Node2,[no_nl]}], + basic_html = [], + cover = [], + config = CFGs, + userconfig = [{Node,{?MODULE,"cfg_str1"}}, + {Node1,{?MODULE,"cfg_str1"}}, + {Node2,{?MODULE,"cfg_str1"}}, + {Node,{?MODULE,"cfg_str2"}}, + {Node1,{?MODULE,"cfg_str2"}}, + {Node2,{?MODULE,"cfg_str2"}}], + event_handler = [{master,evh1,[1]}, + {Node1,evh2,[[2,3]]}, + {Node1,evh3,[[2,3]]}, + {Node2,evh2,[[2,3]]}, + {Node2,evh3,[[2,3]]}], + ct_hooks = [{Node,{cth_mod1,[]}}, + {Node1,{cth_mod1,[]}}, + {Node2,{cth_mod1,[]}}, + {Node,{cth_mod2,[]}}, + {Node1,{cth_mod2,[]}}, + {Node2,{cth_mod2,[]}}], + enable_builtin_hooks = true, + noinput = false, + include = Incls, + auto_compile = [], + stylesheet = [], + multiply_timetraps = [{Node1,2}], + scale_timetraps = [], + create_priv_dir = [], + tests = [{{Node1,TO1V}, + [{x_SUITE,[all]}, + {y_SUITE,[{g1,all},{g2,all},tc1,tc2]}, + {z_SUITE,[{all,{skip,"skipped"}}]}]}, + {{Node2,TO2V}, + [{x_SUITE,[all, + {{g1,all},{skip,"skipped"}}, + {{g2,all},{skip,"skipped"}}]}, + {y_SUITE,[all, + {tc1,{skip,"skipped"}}, + {tc2,{skip,"skipped"}}]}]}], + merge_tests = true}, + + verify_result(Verify,ListResult,FileResult). + +%%%----------------------------------------------------------------- +%%% +unknown_terms(Config) -> + PrivDir = ?config(priv_dir, Config), + + Spec1 = [{suites,PrivDir,all}, + {userdata,"I've got news for you"}], + {error,{undefined_term_in_spec,{userdata,_}}} = + (catch ct_testspec:collect_tests_from_list(Spec1, false)), + true = is_record(ct_testspec:collect_tests_from_list(Spec1, true), + testspec), + + Spec2 = [{logdir,{logdir,PrivDir}}], + {error,{invalid_directory_name,_}} = + (catch ct_testspec:collect_tests_from_list(Spec2, false)), + + Spec3 = [{suite,PrivDir,all}], + {error,{undefined_term_in_spec,{suite,_,_}}} = + (catch ct_testspec:collect_tests_from_list(Spec3, false)), + true = is_record(ct_testspec:collect_tests_from_list(Spec3, true), testspec), + + Spec4 = [{suites,PrivDir,all}, + {skip_suites,PrivDir,x_SUITE}], + {error,{bad_term_in_spec,{skip_suites,_,_}}} = + (catch ct_testspec:collect_tests_from_list(Spec4, false)), + {error,{bad_term_in_spec,{skip_suites,_,_}}} = + (catch ct_testspec:collect_tests_from_list(Spec4, true)), + + Spec5 = [{configs,all_nodes,PrivDir}], + {error,{undefined_term_in_spec,{configs,_,_}}} = + (catch ct_testspec:collect_tests_from_list(Spec5, false)), + true = is_record(ct_testspec:collect_tests_from_list(Spec5, true), testspec), + + ok. + +%%%----------------------------------------------------------------- +%%% +no_merging(_Config) -> + Node1 = node1@host1, + Node2 = node2@host2, + TODir1 = "../tests/to1", + TODir2 = "../tests/to2", + Spec = + [ + {merge_tests,false}, + {node,n1,Node1}, + {node,n2,Node2}, + {suites,n1,TODir1,[x_SUITE]}, + {groups,n1,TODir1,y_SUITE,[g1,g2]}, + {cases,n1,TODir1,y_SUITE,[tc1,tc2]}, + {skip_suites,n1,TODir1,z_SUITE,"skipped"}, + {suites,n2,TODir2,[x_SUITE,y_SUITE]}, + {skip_groups,n2,TODir2,x_SUITE,[g1,g2],"skipped"}, + {skip_cases,n2,TODir2,y_SUITE,[tc1,tc2],"skipped"} + ], + + {ok,SpecDir} = file:get_cwd(), + + ListResult = ct_testspec:collect_tests_from_list(Spec, false), + ct:pal("TESTSPEC RECORD FROM LIST:~n~p~n", [rec2proplist(ListResult)]), + SpecFile = ct_test_support:write_testspec(Spec,SpecDir, + "no_merging.spec"), + FileResult = ct_testspec:collect_tests_from_file([SpecFile], false), + ct:pal("TESTSPEC RECORD FROM FILE:~n~p~n", [rec2proplist(FileResult)]), + + Node = node(), + TO1V = get_absdir(filename:join(SpecDir,TODir1)), + TO2V = get_absdir(filename:join(SpecDir,TODir2)), + + Verify = #testspec{merge_tests = false, + spec_dir = SpecDir, + nodes = [{undefined,Node},{n1,Node1},{n2,Node2}], + tests = [{{Node1,TO1V}, + [{x_SUITE,[all]}]}, + {{Node1,TO1V}, + [{y_SUITE,[{g1,all},{g2,all}]}]}, + {{Node1,TO1V}, + [{y_SUITE,[tc1,tc2]}]}, + {{Node1,TO1V}, + [{z_SUITE,[{all,{skip,"skipped"}}]}]}, + {{Node2,TO2V}, + [{x_SUITE,[all]}]}, + {{Node2,TO2V}, + [{y_SUITE,[all]}]}, + {{Node2,TO2V}, + [{x_SUITE,[{{g1,all},{skip,"skipped"}}, + {{g2,all},{skip,"skipped"}}]}]}, + {{Node2,TO2V}, + [{y_SUITE,[{tc1,{skip,"skipped"}}, + {tc2,{skip,"skipped"}}]}]}]}, + + verify_result(Verify,ListResult,FileResult). + +%%%----------------------------------------------------------------- +%%% +multiple_specs(_Config) -> + Node1 = node1@host1, + Node2 = node2@host2, + TODir1 = "../tests/to1", + TODir2 = "../tests/to2", + CfgDir1 = "../cfgs/to1/x.cfg", + CfgDir2 = ["../cfgs/to2/x.cfg","../cfgs/to2/y.cfg"], + LogDir = "../logs", + Spec1 = + [ + {node,n1,Node1}, + {node,n2,Node2}, + {alias,to1,TODir1}, + {alias,to2,TODir2}, + {label,"multiple_specs1"}, + {config,n1,CfgDir1}, + {config,n2,CfgDir2}, + {logdir,all_nodes,LogDir} + ], + Spec2 = + [ + {merge_tests,false}, + {label,"multiple_specs2"}, + {suites,n1,TODir1,[x_SUITE]}, + {groups,n1,TODir1,y_SUITE,[g1,g2]}, + {cases,n1,TODir1,y_SUITE,[tc1,tc2]}, + {skip_suites,n1,TODir1,z_SUITE,"skipped"}, + {suites,n2,TODir2,[x_SUITE,y_SUITE]}, + {skip_groups,n2,TODir2,x_SUITE,[g1,g2],"skipped"}, + {skip_cases,n2,TODir2,y_SUITE,[tc1,tc2],"skipped"} + ], + + {ok,SpecDir} = file:get_cwd(), + SpecFile1 = ct_test_support:write_testspec(Spec1,SpecDir, + "multiple_specs.1.spec"), + SpecFile2 = ct_test_support:write_testspec(Spec2,SpecDir, + "multiple_specs.2.spec"), + FileResult = ct_testspec:collect_tests_from_file([SpecFile1,SpecFile2], + false), + ct:pal("TESTSPEC RECORD FROM FILE:~n~p~n", [rec2proplist(FileResult)]), + + Node = node(), + TO1V = get_absdir(filename:join(SpecDir,TODir1)), + TO2V = get_absdir(filename:join(SpecDir,TODir2)), + CFGs = [{Node1,get_absdir(filename:join(SpecDir,CfgDir1))} | + [{Node2,get_absdir(filename:join(SpecDir,CfgDir))} || CfgDir <- CfgDir2]], + LogDirV = get_absdir(filename:join(SpecDir,"../logs")), + + Verify = #testspec{merge_tests = false, + spec_dir = SpecDir, + nodes = [{undefined,Node},{n1,Node1},{n2,Node2}], + alias = [{to1,TO1V},{to2,TO2V}], + label = [{Node,"multiple_specs1"}, + {Node1,"multiple_specs1"}, + {Node2,"multiple_specs1"}], + logdir = [{Node,LogDirV},{Node1,LogDirV},{Node2,LogDirV},"."], + config = CFGs, + tests = [{{Node1,TO1V}, + [{x_SUITE,[all]}]}, + {{Node1,TO1V}, + [{y_SUITE,[{g1,all},{g2,all}]}]}, + {{Node1,TO1V}, + [{y_SUITE,[tc1,tc2]}]}, + {{Node1,TO1V}, + [{z_SUITE,[{all,{skip,"skipped"}}]}]}, + {{Node2,TO2V}, + [{x_SUITE,[all]}]}, + {{Node2,TO2V}, + [{y_SUITE,[all]}]}, + {{Node2,TO2V}, + [{x_SUITE,[{{g1,all},{skip,"skipped"}}, + {{g2,all},{skip,"skipped"}}]}]}, + {{Node2,TO2V}, + [{y_SUITE,[{tc1,{skip,"skipped"}}, + {tc2,{skip,"skipped"}}]}]}]}, + + verify_result(Verify,FileResult,FileResult). + +%%%----------------------------------------------------------------- +%%% +misc_config_terms(_Config) -> + CfgDir = "../cfgs/to1", + + Spec = + [{node,x,n1@h1},{node,y,n2@h2}, + + {config,CfgDir,"a.cfg"}, + {config,n1@h1,CfgDir,"b.cfg"}, + {config,all_nodes,CfgDir,"c.cfg"}, + {config,all_nodes,filename:join(CfgDir,"d.cfg")}, + + {basic_html,true}, + {basic_html,n1@h1,false}, + {basic_html,n2@h2,true}, + + {enable_builtin_hooks,false}, + + {noinput,true}, + + {auto_compile,false}, + {auto_compile,n1@h1,true}, + {auto_compile,n2@h2,false}, + + {stylesheet,"../css"}, + {stylesheet,n1@h1,"./n1/css"}, + {stylesheet,n2@h2,"./n2/css"}, + + {create_priv_dir,[auto_per_tc]}, + {create_priv_dir,n1@h1,[manual_per_tc]}, + {create_priv_dir,n2@h2,[auto_per_run]} + ], + + {ok,SpecDir} = file:get_cwd(), + + ListResult = ct_testspec:collect_tests_from_list(Spec, false), + ct:pal("TESTSPEC RECORD FROM LIST:~n~p~n", [rec2proplist(ListResult)]), + SpecFile = ct_test_support:write_testspec(Spec,SpecDir, + "misc_config_terms.spec"), + FileResult = ct_testspec:collect_tests_from_file([SpecFile], false), + ct:pal("TESTSPEC RECORD FROM FILE:~n~p~n", [rec2proplist(FileResult)]), + + Node = node(), + CfgA = get_absdir(filename:join(filename:join(SpecDir,CfgDir), "a.cfg")), + CfgB = get_absdir(filename:join(filename:join(SpecDir,CfgDir), "b.cfg")), + CfgC = get_absdir(filename:join(filename:join(SpecDir,CfgDir), "c.cfg")), + CfgD = get_absdir(filename:join(filename:join(SpecDir,CfgDir), "d.cfg")), + CSS = get_absdir(filename:join(SpecDir,"../css")), + CSS1 = get_absdir(filename:join(SpecDir,"./n1/css")), + CSS2 = get_absdir(filename:join(SpecDir,"./n2/css")), + + Verify = #testspec{spec_dir = SpecDir, + nodes = [{undefined,Node},{x,n1@h1},{y,n2@h2}], + basic_html = [{Node,true},{n1@h1,false},{n2@h2,true}], + config = [{Node,CfgA}, + {n1@h1,CfgA}, + {n2@h2,CfgA}, + {n1@h1,CfgB}, + {Node,CfgC}, + {n1@h1,CfgC}, + {n2@h2,CfgC}, + {Node,CfgD}, + {n1@h1,CfgD}, + {n2@h2,CfgD}], + enable_builtin_hooks = false, + noinput = true, + auto_compile = [{Node,false}, + {n1@h1,true}, + {n2@h2,false}], + stylesheet = [{Node,CSS}, + {n1@h1,CSS1}, + {n2@h2,CSS2}], + create_priv_dir = [{Node,[auto_per_tc]}, + {n1@h1,[manual_per_tc]}, + {n2@h2,[auto_per_run]}] + }, + + verify_result(Verify,ListResult,FileResult). + +%%%----------------------------------------------------------------- +%%% +define_names_1(_Config) -> + Spec = + [ + {define,'HOST','eniac'}, + {define,'NODE1',testnode1}, + {define,'NODE2',testnode2}, + {define,'NODES',['NODE1@HOST', + 'NODE2@HOST']}, + {define,'TOPDIR',".."}, + {define,'TO1',"to1"}, + {define,'TO2',"to2"}, + {define,'LOGDIR',"'TOPDIR'/logdir"}, + {define,'LOGDIR1',"'TOPDIR'/logdir1"}, + {define,'LOGDIR2',"'TOPDIR'/logdir2"}, + {define,'CFGDIR',"'TOPDIR'/cfgs"}, + {define,'CFGFILES',["cfgX","cfgY"]}, + {define,'TESTDIR',"'TOPDIR'/test"}, + {define,'TO1DIR',"'TESTDIR'/'TO1'"}, + {define,'TO2DIR',"'TESTDIR'/'TO2'"}, + {define,'EXSUITE',ex_SUITE}, + {define,'EXGRS',[g1,g2]}, + + {logdir,'LOGDIR'}, + {logdir,'NODE1@HOST','LOGDIR1'}, + {logdir,'NODE2@HOST','LOGDIR2'}, + + {config,["a.cfg","b.cfg"]}, + {config,'NODES',"./'CFGDIR'/c.cfg"}, + {config,'CFGDIR',["d.cfg","e.cfg"]}, + {config,'NODE2@HOST','CFGDIR','CFGFILES'}, + + {suites,'NODE1@HOST','TO1DIR',all}, + {suites,'NODES','TO2DIR',all}, + + {groups,'TO1DIR','EXSUITE','EXGRS'} + ], + + {ok,SpecDir} = file:get_cwd(), + + ListResult = ct_testspec:collect_tests_from_list(Spec, false), + ct:pal("TESTSPEC RECORD FROM LIST:~n~p~n", [rec2proplist(ListResult)]), + SpecFile = ct_test_support:write_testspec(Spec,SpecDir, + "define_names_1.spec"), + FileResult = ct_testspec:collect_tests_from_file([SpecFile], false), + ct:pal("TESTSPEC RECORD FROM FILE:~n~p~n", [rec2proplist(FileResult)]), + + N = node(), + N1 = testnode1@eniac, + N2 = testnode2@eniac, + Join = fun(Dir) -> shorten_path(filename:join(SpecDir,Dir),SpecDir) end, + + Verify = #testspec{spec_dir = SpecDir, + nodes = [{undefined,N2}, + {undefined,N1}, + {undefined,N}], + config = [{N2,Join("a.cfg")},{N2,Join("b.cfg")}, + {N1,Join("a.cfg")},{N1,Join("b.cfg")}, + {N,Join("a.cfg")},{N,Join("b.cfg")}, + {N1,Join("../cfgs/c.cfg")},{N2,Join("../cfgs/c.cfg")}, + {N2,Join("../cfgs/d.cfg")},{N2,Join("../cfgs/e.cfg")}, + {N1,Join("../cfgs/d.cfg")},{N1,Join("../cfgs/e.cfg")}, + {N,Join("../cfgs/d.cfg")},{N,Join("../cfgs/e.cfg")}, + {N2,Join("../cfgs/cfgX")},{N2,Join("../cfgs/cfgY")}], + logdir = [{N,Join("../logdir")}, + {N1,Join("../logdir1")}, + {N2,Join("../logdir2")}, + "."], + tests = [{{N1,Join("../test/to1")},[{all,[all]}]}, + {{N1,Join("../test/to2")},[{all,[all]}]}, + {{N2,Join("../test/to2")},[{all,[all]}]}, + {{N2,Join("../test/to1")}, + [{ex_SUITE,[{g1,all},{g2,all}]}]}, + {{N,Join("../test/to1")}, + [{ex_SUITE,[{g1,all},{g2,all}]}]}] + }, + verify_result(Verify,ListResult,FileResult). + + +%%%----------------------------------------------------------------- +%%% HELP FUNCTIONS +%%%----------------------------------------------------------------- + +verify_result(Verify,ListResult,FileResult) -> + {_,TSLTuples} = rec2proplist(ListResult), + {_,TSFTuples} = rec2proplist(FileResult), + {_,VTuples} = rec2proplist(Verify), + VResult = + (catch lists:foldl(fun({Tag,Val},{[{Tag,Val}|TSL],[{Tag,Val}|TSF]}) -> + {TSL,TSF}; + ({Tag,Val},{[{_Tag,TSLVal}|_TSL],[{Tag,Val}|_TSF]}) -> + exit({ts_list_mismatch,Tag,Val,TSLVal}); + ({Tag,Val},{[{Tag,Val}|_TSL],[{_Tag,TSFVal}|_TSF]}) -> + exit({ts_file_mismatch,Tag,Val,TSFVal}); + ({Tag,Val},{_,[{_Tag,TSFVal}|_TSF]}) -> + exit({ts_mismatch,Tag,Val,TSFVal}) + end, {TSLTuples,TSFTuples}, VTuples)), + case VResult of + {'EXIT',Reason} -> + ct:fail(Reason); + _ -> + ok + end, + ok. + + +check_parameter(S="cfg_str1") -> + {ok,{config,S}}; +check_parameter(S="cfg_str2") -> + {ok,{config,S}}. +read_config(S) -> + {ok,[{cfg,S}]}. + +rec2proplist(E={error,_What}) -> + exit({invalid_testspec_record,E}); +rec2proplist(Rec) -> + [RecName|RecList] = tuple_to_list(Rec), + FieldNames = + if RecName == testspec -> + record_info(fields, testspec); + true -> + undefined + end, + {RecName,combine_names_and_vals(FieldNames,RecList)}. + +combine_names_and_vals([FN|FNs], [V|Vs]) -> + [{FN,V} | combine_names_and_vals(FNs, Vs)]; +combine_names_and_vals([], []) -> + []; +combine_names_and_vals(_, _) -> + []. + +get_absdir(Dir) -> + shorten_path(filename:absname(Dir),Dir). + +shorten_path(Path,SpecDir) -> + case shorten_split_path(filename:split(Path),[]) of + [] -> + [Root|_] = filename:split(SpecDir), + Root; + Short -> + filename:join(Short) + end. + +shorten_split_path([".."|Path],SoFar) -> + shorten_split_path(Path,tl(SoFar)); +shorten_split_path(["."|Path],SoFar) -> + shorten_split_path(Path,SoFar); +shorten_split_path([Dir|Path],SoFar) -> + shorten_split_path(Path,[Dir|SoFar]); +shorten_split_path([],SoFar) -> + lists:reverse(SoFar). |