diff --git a/.CI/Jenkinsfile b/.CI/Jenkinsfile index 1004a57..21ebae7 100644 --- a/.CI/Jenkinsfile +++ b/.CI/Jenkinsfile @@ -3,268 +3,300 @@ pipeline { parameters { booleanParam(name: 'OLDLIBS', defaultValue: false, description: 'Also test some outdated libraries') - booleanParam(name: 'v1_12', defaultValue: false, description: 'maintenance/v1.12 branch (ripper1)') - booleanParam(name: 'v1_13', defaultValue: false, description: 'maintenance/v1.13 branch (ripper1)') - booleanParam(name: 'v1_14', defaultValue: false, description: 'maintenance/v1.14 branch (ripper1)') - booleanParam(name: 'v1_16', defaultValue: false, description: 'maintenance/v1.16 branch (ripper1)') - booleanParam(name: 'v1_17', defaultValue: false, description: 'maintenance/v1.17 branch (ripper1)') - booleanParam(name: 'master', defaultValue: false, description: 'master branch (ripper1)') - - booleanParam(name: 'fmi_v1_12', defaultValue: false, description: 'maintenance/v1.12 branch with FMI (ripper2)') - booleanParam(name: 'fmi_v1_13', defaultValue: false, description: 'maintenance/v1.13 branch with FMI (ripper2)') - booleanParam(name: 'fmi_v1_14', defaultValue: false, description: 'maintenance/v1.14 branch with FMI (ripper2)') - booleanParam(name: 'fmi_v1_16', defaultValue: false, description: 'maintenance/v1.16 branch with FMI (ripper2)') - booleanParam(name: 'fmi_v1_17', defaultValue: false, description: 'maintenance/v1.17 branch with FMI (ripper2)') - booleanParam(name: 'fmi_master', defaultValue: false, description: 'master branch with FMI (ripper2)') - booleanParam(name: 'newInst_daeMode', defaultValue: false, description: 'master branch, --daeMode with -d=newInst (ripper2)') - booleanParam(name: 'oldInst', defaultValue: false, description: 'master branch, with -d=nonewInst (ripper2)') - booleanParam(name: 'report_ripper1', defaultValue: false, description: 'Generate a report for ripper1 without running the tests') - booleanParam(name: 'report_ripper2', defaultValue: false, description: 'Generate a report for ripper2 without running the tests') - - booleanParam(name: 'conversion_script', defaultValue: false, description: 'master branch with conversion script from MSL 3 to 4 (ripper1). This is an experimental job that does not run on a fixed schedule.') - booleanParam(name: 'cvode', defaultValue: false, description: 'master branch, with -d=newInst and -s cvode (ripper2). This is an experimental job that does not run on a fixed schedule.') + booleanParam(name: 'v1_26', defaultValue: false, description: 'maintenance/v1.26 branch (ryzen-5950x-1)') + booleanParam(name: 'v1_27', defaultValue: false, description: 'maintenance/v1.27 branch (ryzen-5950x-1)') + booleanParam(name: 'master', defaultValue: false, description: 'master branch (ryzen-5950x-1)') + + booleanParam(name: 'fmi_v1_26', defaultValue: false, description: 'maintenance/v1.26 branch with FMI, simulated by OMSimulator, filling v1.26-fmi (ryzen-5950x-2). Ticking it together with fmpy_fmi_v1_26 runs one job that builds every FMU once and simulates it with both.') + booleanParam(name: 'fmi_v1_27', defaultValue: false, description: 'maintenance/v1.27 branch with FMI, simulated by OMSimulator, filling v1.27-fmi (ryzen-5950x-2). Ticking it together with fmpy_fmi_v1_27 runs one job that builds every FMU once and simulates it with both.') + booleanParam(name: 'fmi_master', defaultValue: false, description: 'master branch with FMI, simulated by OMSimulator, filling master-fmi (ryzen-5950x-2). Ticking it together with fmpy_fmi_master runs one job that builds every FMU once and simulates it with both.') + + booleanParam(name: 'cs_fmu_cvode_v1_26', defaultValue: false, description: 'maintenance/v1.26 branch with CVODE CS FMUs running OMSimulator (ryzen-5950x-2)') + booleanParam(name: 'cs_fmu_cvode_v1_27', defaultValue: false, description: 'maintenance/v1.27 branch with CVODE CS FMUs running OMSimulator (ryzen-5950x-2)') + booleanParam(name: 'cs_fmu_cvode_master', defaultValue: false, description: 'master branch with CVODE CS FMUs running OMSimulator (ryzen-5950x-2)') + + booleanParam(name: 'fmpy_fmi_v1_26', defaultValue: false, description: 'maintenance/v1.26 branch with FMI, simulated by FMPy, filling v1.26-fmi-fmpy (ryzen-5950x-2). Ticking it together with fmi_v1_26 runs one job that builds every FMU once and simulates it with both.') + booleanParam(name: 'fmpy_fmi_v1_27', defaultValue: false, description: 'maintenance/v1.27 branch with FMI, simulated by FMPy, filling v1.27-fmi-fmpy (ryzen-5950x-2). Ticking it together with fmi_v1_27 runs one job that builds every FMU once and simulates it with both.') + booleanParam(name: 'fmpy_fmi_master', defaultValue: false, description: 'master branch with FMI, simulated by FMPy, filling master-fmi-fmpy (ryzen-5950x-2). Ticking it together with fmi_master runs one job that builds every FMU once and simulates it with both.') + + booleanParam(name: 'newInst_daeMode', defaultValue: false, description: 'master branch, --daeMode with -d=newInst (ryzen-5950x-2)') + booleanParam(name: 'newInst_newBackend', defaultValue: false, description: 'master branch, -d=newInst --newBackend, (ryzen-5950x-1)') + booleanParam(name: 'newBackend_daeMode', defaultValue: false, description: 'master branch, --newBackend --daeMode with -d=newInst (ryzen-5950x-2). This is an experimental job that does not run on a fixed schedule.') + booleanParam(name: 'oldInst', defaultValue: false, description: 'master branch, with -d=nonewInst (ryzen-5950x-2)') + + booleanParam(name: 'cpp_v1_26', defaultValue: false, description: 'maintenance/v1.26 branch, with --simCodeTarget=Cpp (ryzen-5950x-2).') + booleanParam(name: 'cpp_v1_27', defaultValue: false, description: 'maintenance/v1.27 branch, with --simCodeTarget=Cpp (ryzen-5950x-2).') + booleanParam(name: 'cpp', defaultValue: false, description: 'master branch, with --simCodeTarget=Cpp (ryzen-5950x-2).') + booleanParam(name: 'report_ryzen_5950x_1', defaultValue: false, description: 'Generate a report for ryzen-5950x-1 without running the tests') + booleanParam(name: 'report_ryzen_5950x_2', defaultValue: false, description: 'Generate a report for ryzen-5950x-2 without running the tests') + + booleanParam(name: 'conversion_script', defaultValue: false, description: 'master branch with conversion script from MSL 3 to 4 (ryzen-5950x-1). This is an experimental job that does not run on a fixed schedule.') + booleanParam(name: 'cvode', defaultValue: false, description: 'master branch, with -d=newInst and -s cvode (ryzen-5950x-2). This is an experimental job that does not run on a fixed schedule.') + booleanParam(name: 'gbode', defaultValue: false, description: 'master branch, with -d=newInst and -s gbode (ryzen-5950x-2). This is an experimental job that does not run on a fixed schedule.') + booleanParam(name: 'ida', defaultValue: false, description: 'master branch, with -d=newInst and -s ida (ryzen-5950x-2). This is an experimental job that does not run on a fixed schedule.') + booleanParam(name: 'generateSymbolicJacobian', defaultValue: false, description: 'master branch, with --generateSymbolicJacobian (ryzen-5950x-1). This is an experimental job that does not run on a fixed schedule.') + booleanParam(name: 'wasm_jit', defaultValue: false, description: 'master branch, with --simCodeTarget=wasm-jit (ryzen-5950x-2). This is an experimental job that does not run on a fixed schedule.') + booleanParam(name: 'heavy_tests', defaultValue: false, description: 'master branch, runs one test at a time. That is, no parallel launching of tests. omc will use multiple threads for each test (-n=1 is not set unlike the other regression tests.), (ryzen-5950x-1). This is an experimental job that does not run on a fixed schedule.') + + string(name: 'pull_request', defaultValue: '', description: 'Test an OpenModelica pull request rather than a branch: its number, e.g. 16354. omc is built from refs/pull//merge - the pull request as it would land - the results fill a pr- table, and the report compares them against the newest run of pull_request_baseline. Left empty, nothing of this runs.') + string(name: 'pull_request_baseline', defaultValue: 'master', description: 'The branch a pull request is compared against. Its newest run has to come from pull_request_node, or the report compares the machines as much as the pull request.') + string(name: 'pull_request_config', defaultValue: 'configs/conf.json', description: 'What a pull request run tests. A full run takes days, so a smaller configuration file is often the better question to ask.') + choice(name: 'pull_request_node', choices: ['ryzen-5950x-1', 'ryzen-5950x-2-1', 'ryzen-9950x'], description: 'The machine a pull request runs on. The default is the one that produces the master runs it is compared against.') + booleanParam(name: 'drop_stale_pull_request_tables', defaultValue: false, description: 'Drop the pr- tables of pull requests that have been merged or closed, and of those tested more than 60 days ago. The reports published for them are not touched.') } environment { LC_ALL = 'C.UTF-8' + // Where the results go. The scripts take it from here instead of a --db + // option on every single invocation. + LIBTEST_DB = 'postgresql://om@openmodelica.org/omdb' } stages { stage('test') { parallel { - stage('v1.12') { + stage('v1.26') { agent { node { - label 'ripper.openmodelica.org' + label 'ryzen-5950x-1' customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } options { skipDefaultCheckout() } when { beforeAgent true - expression { params.v1_12 } + expression { params.v1_26 } } steps { - test('maintenance/v1.12', 'v1.12', '', '', 'LibraryTestingRipper1DB', true, '', true, false) + runRegressiontest('maintenance/v1.26', 'v1.26', '', '', false, '', '', false, false) } } - stage('v1.13') { + stage('v1.27') { agent { node { - label 'ripper.openmodelica.org' + label 'ryzen-5950x-1' customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } options { skipDefaultCheckout() } when { beforeAgent true - expression { params.v1_13 } + expression { params.v1_27 } } steps { - test('maintenance/v1.13', 'v1.13', '', '', 'LibraryTestingRipper1DB', true, '', true, false) + runRegressiontest('maintenance/v1.27', 'v1.27', '', '', false, '', '', false, false) } } - stage('v1.14') { + stage('master') { agent { node { - label 'ripper.openmodelica.org' + label 'ryzen-5950x-1' customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } options { skipDefaultCheckout() } when { beforeAgent true - expression { params.v1_14 } + expression { params.master } } steps { - test('maintenance/v1.14', 'v1.14', '', '', 'LibraryTestingRipper1DB', false, '', true, false) + runRegressiontest('master', 'master', '', '', false, '', '', false, false) } } - stage('v1.16') { + stage('conversion script') { agent { node { - label 'ripper.openmodelica.org' + label 'ryzen-5950x-1' customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } options { skipDefaultCheckout() } when { beforeAgent true - expression { params.v1_16 } + expression { params.conversion_script } } steps { - test('maintenance/v1.16', 'v1.16', '', '', 'LibraryTestingRipper1DB', false, '', false, false) + runRegressiontest('master', 'conversion', '', '', false, '', '', false, true) } } - stage('v1.17') { + stage('pull request') { agent { node { - label 'ripper.openmodelica.org' + label "${params.pull_request_node}" customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } options { skipDefaultCheckout() } when { beforeAgent true - expression { params.v1_17 } + expression { params.pull_request.trim() } } steps { - test('maintenance/v1.17', 'v1.17', '', '', 'LibraryTestingRipper1DB', false, '', false, false) + script { + if (!(params.pull_request.trim() ==~ /[0-9]+/)) { + error "pull_request is a pull request number; got '${params.pull_request}'" + } + } + // One build of omc per pull request is kept, as for a branch, and they + // accumulate: a pull request is tested once and never again. + sh 'find "$HOME/saved_omc" -maxdepth 1 -name "pr-*" -type d -mtime +14 -exec rm -rf {} ";" || true' + runRegressiontest("pr-${params.pull_request.trim()}", "pr-${params.pull_request.trim()}", '', '', false, '', '', false, false, 0, params.pull_request_config) } } - stage('master') { + stage('newInst-newBackend') { agent { node { - label 'ripper.openmodelica.org' + label 'ryzen-5950x-1' customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } options { skipDefaultCheckout() } when { beforeAgent true - expression { params.master } + expression { params.newInst_newBackend } } steps { - test('master', 'master', '', '', 'LibraryTestingRipper1DB', false, '', false, false) + runRegressiontest('master', 'newInst-newBackend', 'setCommandLineOptions("-d=newInst,-frontEndUnitCheck --newBackend")', '', false, '', '', false, false) } } - stage('conversion script') { + stage('v1.26 FMI') { agent { node { - label 'ripper.openmodelica.org' + label 'ryzen-5950x-2-1' customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } options { skipDefaultCheckout() } when { beforeAgent true - expression { params.conversion_script } + expression { params.fmi_v1_26 || params.fmpy_fmi_v1_26 } } steps { - test('master', 'conversion', '', '', 'LibraryTestingRipper1DB', false, '', false, true) + runRegressiontest('maintenance/v1.26', 'v1.26-fmi', '', omsimulatorHash(), false, '', '', false, false, 0, 'configs/conf.json', '', '', fmiSimulators(params.fmi_v1_26, params.fmpy_fmi_v1_26)) } } - - stage('v1.12 FMI') { + stage('v1.27 FMI') { agent { node { - label 'ripper2-1' + label 'ryzen-5950x-2-1' customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } options { skipDefaultCheckout() } when { beforeAgent true - expression { params.fmi_v1_12 } + expression { params.fmi_v1_27 || params.fmpy_fmi_v1_27 } } steps { - test('maintenance/v1.12', 'v1.12-fmi', '', omsimulatorHash(), 'LibraryTestingRipper2DB', true, '', true, false) + runRegressiontest('maintenance/v1.27', 'v1.27-fmi', '', omsimulatorHash(), false, '', '', false, false, 0, 'configs/conf.json', '', '', fmiSimulators(params.fmi_v1_27, params.fmpy_fmi_v1_27)) } } - stage('v1.13 FMI') { + stage('master FMI') { agent { node { - label 'ripper2-1' + label 'ryzen-5950x-2-1' customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } options { skipDefaultCheckout() } when { beforeAgent true - expression { params.fmi_v1_13 } + expression { params.fmi_master || params.fmpy_fmi_master } } steps { - test('maintenance/v1.13', 'v1.13-fmi', '', omsimulatorHash(), 'LibraryTestingRipper2DB', true, '', true, false) + runRegressiontest('master', 'master-fmi', '', 'origin/master', false, '', '', false, false, 0, 'configs/conf.json', '', '', fmiSimulators(params.fmi_master, params.fmpy_fmi_master)) } } - stage('v1.14 FMI') { + + stage('v1.26 CVODE CS-FMUs with OMSimulator') { agent { node { - label 'ripper2-1' + label 'ryzen-5950x-2-1' customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } options { skipDefaultCheckout() } when { beforeAgent true - expression { params.fmi_v1_14 } + expression { params.cs_fmu_cvode_v1_26 } } steps { - test('maintenance/v1.14', 'v1.14-fmi', '', omsimulatorHash(), 'LibraryTestingRipper2DB', false, '', true, false) + runRegressiontest('maintenance/v1.26', 'v1.26-cs-fmu-cvode', 'setCommandLineOptions("--fmiFlags=s:cvode --fmuRuntimeDepends=modelica")', omsimulatorHash(), false, '', '', false, false) } } - stage('v1.16 FMI') { + stage('v1.27 CVODE CS-FMUs with OMSimulator') { agent { node { - label 'ripper2-1' + label 'ryzen-5950x-2-1' customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } options { skipDefaultCheckout() } when { beforeAgent true - expression { params.fmi_v1_16 } + expression { params.cs_fmu_cvode_v1_27 } } steps { - test('maintenance/v1.16', 'v1.16-fmi', '', omsimulatorHash(), 'LibraryTestingRipper2DB', false, '', false, false) + runRegressiontest('maintenance/v1.27', 'v1.27-cs-fmu-cvode', 'setCommandLineOptions("--fmiFlags=s:cvode --fmuRuntimeDepends=modelica")', omsimulatorHash(), false, '', '', false, false) } } - stage('v1.17 FMI') { + stage('master CVODE CS-FMUs with OMSimulator') { agent { node { - label 'ripper2-1' + label 'ryzen-5950x-2-1' customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } options { skipDefaultCheckout() } when { beforeAgent true - expression { params.fmi_v1_17 } + expression { params.cs_fmu_cvode_master } } steps { - test('maintenance/v1.17', 'v1.17-fmi', '', omsimulatorHash(), 'LibraryTestingRipper2DB', false, '', false, false) + runRegressiontest('master', 'master-cs-fmu-cvode', 'setCommandLineOptions("--fmiFlags=s:cvode --fmuRuntimeDepends=modelica")', 'origin/master', false, '', '', false, false) } } - stage('master FMI') { + + stage('newInst-daeMode') { agent { node { - label 'ripper2-1' + label 'ryzen-5950x-2-1' customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } options { skipDefaultCheckout() } when { beforeAgent true - expression { params.fmi_master } + expression { params.newInst_daeMode } } steps { - test('master', 'master-fmi', '', 'origin/master', 'LibraryTestingRipper2DB', false, '', false, false) + runRegressiontest('master', 'newInst-daeMode', 'setCommandLineOptions("-d=newInst,-frontEndUnitCheck --daeMode=true")', '', false, '', '', false, false) } } - stage('newInst-daeMode') { + stage('newBackend-daeMode') { agent { node { - label 'ripper2-1' + label 'ryzen-5950x-2-1' customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } options { skipDefaultCheckout() } when { beforeAgent true - expression { params.newInst_daeMode } + expression { params.newBackend_daeMode } } steps { - test('master', 'newInst-daeMode', 'setCommandLineOptions("-d=newInst,-frontEndUnitCheck --daeMode=true")', '', 'LibraryTestingRipper2DB', false, '', false, false) + runRegressiontest('master', 'newBackend-daeMode', 'setCommandLineOptions("-d=newInst,-frontEndUnitCheck --newBackend --daeMode=true")', '', false, '', '', false, false) } } stage('oldInst') { agent { node { - label 'ripper2-1' + label 'ryzen-5950x-2-1' customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } @@ -274,13 +306,13 @@ pipeline { expression { params.oldInst } } steps { - test('master', 'oldInst', 'setCommandLineOptions("-d=nonewInst")', '', 'LibraryTestingRipper2DB', false, '', false, false) + runRegressiontest('master', 'oldInst', 'setCommandLineOptions("-d=nonewInst")', '', false, '', '', false, false) } } stage('cvode') { agent { node { - label 'ripper2-1' + label 'ryzen-5950x-2-1' customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } @@ -290,180 +322,323 @@ pipeline { expression { params.cvode } } steps { - test('master', 'cvode', 'setCommandLineOptions("-d=newInst,-frontEndUnitCheck")', '', 'LibraryTestingRipper2DB', false, '-s cvode', false, false) + runRegressiontest('master', 'cvode', 'setCommandLineOptions("-d=newInst,-frontEndUnitCheck")', '', false, '-s cvode', '', false, false) } } - } } - stage('report') { parallel { - stage('lib') { + stage('gbode') { + agent { + node { + label 'ryzen-5950x-2-1' + customWorkspace 'ws/OpenModelicaLibraryTestingWork' + } + } + options { skipDefaultCheckout() } + when { + beforeAgent true + expression { params.gbode } + } + steps { + runRegressiontest('master', 'gbode', 'setCommandLineOptions("-d=newInst,-frontEndUnitCheck")', '', false, '-s gbode -gbm=radauIIA3', '', false, false) + } + } + stage('ida') { agent { - dockerfile { - label 'linux' - dir '.CI/build-dep' - customWorkspace 'ws/OpenModelicaLibraryTestingReport' + node { + label 'ryzen-5950x-2-1' + customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } + options { skipDefaultCheckout() } when { beforeAgent true - expression { params.v1_12 || params.v1_13 || params.v1_14 || params.v1_16 || params.v1_17 || params.master || params.conversion_script || params.report_ripper1 } + expression { params.ida } + } + steps { + runRegressiontest('master', 'ida', 'setCommandLineOptions("-d=newInst,-frontEndUnitCheck")', '', false, '-s ida', '', false, false) + } + } + stage('wasm-jit') { + agent { + node { + label 'ryzen-9950x' + customWorkspace 'ws/OpenModelicaLibraryTestingWork' + } } - environment { - GITBRANCHES = 'maintenance/v1.12 maintenance/v1.13 maintenance/v1.14 maintenance/v1.16 maintenance/v1.17 master' - PYTHONIOENCODING = 'utf-8' + options { skipDefaultCheckout() } + when { + beforeAgent true + expression { params.wasm_jit } } steps { - sh 'rm -rf sqlite3.db *.html history' - sh ''' - if ! test -d OpenModelica; then - git clone https://openmodelica.org/git-readonly/OpenModelica.git - fi - cd OpenModelica - git fetch - ''' - sh 'wget -q https://libraries.openmodelica.org/sqlite3/ripper1/sqlite3.db' - sh './clean-empty-omcversion-dates.py' - - sh "./all-reports.py --email --omcgitdir=OpenModelica ${env.GITBRANCHES}" - sh "./all-plots.py ${env.GITBRANCHES}" - - sh "./report.py --branches='${env.GITBRANCHES}' configs/conf-c++.json" - sh 'mv overview.html overview-c++.html' - sh "./report.py --branches='${env.GITBRANCHES}' configs/conf.json configs/conf-c++.json configs/conf-old.json configs/conf-nonstandard.json" - sh 'mv overview.html overview-combined.html' - sh "./report.py --branches='${env.GITBRANCHES}' configs/conf-old.json" - sh "mv overview.html overview-old-libs.html" - sh "./report.py --branches='${env.GITBRANCHES}' configs/conf-nonstandard.json" - sh "mv overview.html overview-nonstandard-libs.html" - sh "./report.py --branches='master conversion' configs/conf.json" - sh "mv overview.html overview-special-jobs.html" - sh "./report.py --branches='${env.GITBRANCHES}' configs/conf.json" - - sh 'date' - sh 'find overview*.html history -type f | wc -l' - sh 'find overview*.html history' - - sshPublisher(publishers: [sshPublisherDesc(configName: 'LibraryTestingReports', transfers: [sshTransfer(sourceFiles: 'overview*.html,history/**')])]) - - } - } - - stage('ripper') { + runRegressiontest('master', 'wasm-jit', 'setCommandLineOptions("--simCodeTarget=wasm-jit")', '', false, '', '--nobuildmodel', false, false, 0, 'configs/conf.json', + '-DOM_OMC_ENABLE_RUST=ON -DRUST_OMC_CI=ON -DRUST_OMC_THREADS=4 -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache', + '.CI/wasm-jit') + } + } + stage('generateSymbolicJacobian') { agent { - dockerfile { - label 'linux' - dir '.CI/build-dep' - customWorkspace 'ws/OpenModelicaLibraryTestingReport' + node { + label 'ryzen-5950x-1' + customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } + options { skipDefaultCheckout() } when { beforeAgent true - expression { params.fmi_v1_12 || params.fmi_v1_13 || params.fmi_v1_14 || params.fmi_v1_16 || params.fmi_v1_17 || params.fmi_master || params.newInst_daeMode || params.oldInst || params.report_ripper2 || params.cvode } + expression { params.generateSymbolicJacobian } + } + steps { + runRegressiontest('master', 'generateSymbolicJacobian', 'setCommandLineOptions("--generateSymbolicJacobian")', '', false, '', '', false, false) + } + } + stage('heavy_tests') { + agent { + node { + label 'ryzen-5950x-1' + customWorkspace 'ws/OpenModelicaLibraryTestingWork' + } } - environment { - GITBRANCHES_FMI = 'maintenance/v1.12-fmi maintenance/v1.13-fmi maintenance/v1.14-fmi maintenance/v1.16-fmi maintenance/v1.17-fmi master-fmi' - GITBRANCHES_NEWINST = 'oldInst' - GITBRANCHES_DAE = 'daemode newInst-daemode' - PYTHONIOENCODING = 'utf-8' + options { skipDefaultCheckout() } + when { + beforeAgent true + expression { params.heavy_tests } } steps { - sh 'rm -f sqlite3.db *.html' - sh ''' - if ! test -d OpenModelica; then - git clone https://openmodelica.org/git-readonly/OpenModelica.git - fi - cd OpenModelica - git fetch - ''' - sh 'wget -q https://libraries.openmodelica.org/sqlite3/ripper2/sqlite3.db' - sh './clean-empty-omcversion-dates.py' - - sh "./all-reports.py --email --omcgitdir=OpenModelica ${env.GITBRANCHES_FMI} ${env.GITBRANCHES_NEWINST} ${env.GITBRANCHES_DAE}" - sh "./all-plots.py ${env.GITBRANCHES_FMI} ${env.GITBRANCHES_NEWINST} ${env.GITBRANCHES_DAE}" - - sh "./report.py --branches='${env.GITBRANCHES_NEWINST}' configs/conf.json" - sh "mv overview.html overview-oldinst.html" - sh "./report.py --branches='${env.GITBRANCHES_NEWINST}' configs/conf.json configs/conf-c++.json configs/conf-old.json configs/conf-nonstandard.json" - sh "mv overview.html overview-combined-oldinst.html" - sh "./report.py --branches='${env.GITBRANCHES_NEWINST}' configs/conf-old.json" - sh "mv overview.html overview-old-libs-oldinst.html" - sh "./report.py --branches='${env.GITBRANCHES_NEWINST}' configs/conf-nonstandard.json" - sh "mv overview.html overview-nonstandard-libs-oldinst.html" - sh "./report.py --branches='${env.GITBRANCHES_NEWINST}' configs/conf-c++.json" - sh "mv overview.html overview-c++-oldinst.html" - - sh "./report.py --branches='${env.GITBRANCHES_FMI}' configs/conf.json" - sh "mv overview.html overview-fmi.html" - sh "./report.py --branches='${env.GITBRANCHES_FMI}' configs/conf-c++.json" - sh "mv overview.html overview-c++-fmi.html" - sh "./report.py --branches='${env.GITBRANCHES_FMI}' configs/conf.json configs/conf-c++.json configs/conf-old.json configs/conf-nonstandard.json" - sh "mv overview.html overview-combined-fmi.html" - sh "./report.py --branches='${env.GITBRANCHES_FMI}' configs/conf-old.json" - sh "mv overview.html overview-old-libs-fmi.html" - sh "./report.py --branches='${env.GITBRANCHES_FMI}' configs/conf-nonstandard.json" - sh "mv overview.html overview-nonstandard-libs-fmi.html" - - sh "./report.py --branches='${env.GITBRANCHES_DAE}' configs/conf.json" - sh "mv overview.html overview-dae.html" - sh "./report.py --branches='${env.GITBRANCHES_DAE}' configs/conf-c++.json" - sh "mv overview.html overview-c++-dae.html" - sh "./report.py --branches='${env.GITBRANCHES_DAE}' configs/conf.json configs/conf-c++.json configs/conf-old.json configs/conf-nonstandard.json" - sh "mv overview.html overview-combined-dae.html" - sh "./report.py --branches='${env.GITBRANCHES_DAE}' configs/conf-old.json" - sh "mv overview.html overview-old-libs-dae.html" - sh "./report.py --branches='${env.GITBRANCHES_DAE}' configs/conf-nonstandard.json" - sh "mv overview.html overview-nonstandard-libs-dae.html" - - sh "./report.py --branches='cvode' configs/conf.json" - sh "mv overview.html overview-cvode.html" - - sh 'date' - sh 'find overview*.html history -type f | wc -l' - sh 'find overview*.html history' - - sshPublisher(publishers: [sshPublisherDesc(configName: 'LibraryTestingReports', transfers: [sshTransfer(sourceFiles: 'overview-*.html,history/**')])]) + runRegressiontest('master', 'heavy_tests', '', '', false, '', '', false, false, 1, 'configs/heavy_tests.json') + } + } + stage('C++ v1.26') { + agent { + node { + label 'ryzen-5950x-2-1' + customWorkspace 'ws/OpenModelicaLibraryTestingWork' + } + } + options { skipDefaultCheckout() } + when { + beforeAgent true + expression { params.cpp_v1_26 } + } + steps { + runRegressiontest('maintenance/v1.26', 'v1.26-cpp', 'setCommandLineOptions("--simCodeTarget=Cpp")', '', false, '', '', false, false) } } - } } - stage('cleanup') { parallel { - // The cleanup is needed because we can't save the cleanup done in the report stage - stage('lib') { + stage('C++ v1.27') { agent { node { - label 'libraries.openmodelica.org' + label 'ryzen-5950x-2-1' + customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } + options { skipDefaultCheckout() } when { beforeAgent true - expression { params.v1_12 || params.v1_13 || params.v1_14 || params.v1_16 || params.v1_17 || params.master || params.report_ripper1 } + expression { params.cpp_v1_27 } } steps { - sh 'cp ~/TEST_LIBS_BACKUP/sqlite3.db .' - sh './clean-empty-omcversion-dates.py' - sh 'cp sqlite3.db ~/TEST_LIBS_BACKUP/' + runRegressiontest('maintenance/v1.27', 'v1.27-cpp', 'setCommandLineOptions("--simCodeTarget=Cpp")', '', false, '', '', false, false) } } - stage('ripper2') { + + stage('C++') { agent { node { - label 'ripper2-1' + label 'ryzen-5950x-2-1' + customWorkspace 'ws/OpenModelicaLibraryTestingWork' } } + options { skipDefaultCheckout() } when { beforeAgent true - expression { params.fmi_v1_12 || params.fmi_v1_13 || params.fmi_v1_14 || params.fmi_v1_16 || params.fmi_v1_17 || params.fmi_master || params.newInst_daeMode || params.oldInst || params.report_ripper2 } + expression { params.cpp } } steps { - sh 'cp ~/TEST_LIBS_BACKUP/sqlite3.db .' - sh './clean-empty-omcversion-dates.py' - sh 'cp sqlite3.db ~/TEST_LIBS_BACKUP/' + runRegressiontest('master', 'cpp', 'setCommandLineOptions("--simCodeTarget=Cpp")', '', false, '', '', false, false) } } } } + stage('report') { + agent { + dockerfile { + label 'linux' + dir '.CI/build-dep' + customWorkspace 'ws/OpenModelicaLibraryTestingReport' + args '-e IDA_EMAIL_USR=$IDA_EMAIL_USR -e IDA_EMAIL_PSW=$IDA_EMAIL_PSW' + } + } + when { + beforeAgent true + expression { params.v1_25 || params.v1_26 || params.v1_27 || params.master || params.conversion_script || params.report_ryzen_5950x_1 || params.report_ryzen_5950x_2 || params.newInst_newBackend || params.generateSymbolicJacobian || params.heavy_tests || params.fmi_v1_25 || params.fmi_v1_26 || params.fmi_v1_27 || params.fmi_master || params.fmpy_fmi_v1_25 || params.fmpy_fmi_v1_26 || params.fmpy_fmi_v1_27 || params.fmpy_fmi_master || params.newInst_daeMode || params.newBackend_daeMode || params.oldInst || params.cpp || params.cvode || params.gbode || params.ida || params.wasm_jit} + } + environment { + GITBRANCHES = 'maintenance/v1.20 maintenance/v1.21 maintenance/v1.22 maintenance/v1.23 maintenance/v1.24 maintenance/v1.25 maintenance/v1.26 maintenance/v1.27 master newInst-newBackend' + GITBRANCHES_FMI = 'maintenance/v1.20-fmi maintenance/v1.21-fmi maintenance/v1.22-fmi maintenance/v1.23-fmi maintenance/v1.24-fmi maintenance/v1.25-fmi maintenance/v1.26-fmi maintenance/v1.27-fmi maintenance/v1.22-fmi-fmpy maintenance/v1.23-fmi-fmpy maintenance/v1.24-fmi-fmpy maintenance/v1.25-fmi-fmpy maintenance/v1.26-fmi-fmpy maintenance/v1.27-fmi-fmpy master-fmi master-fmi-fmpy' + GITBRANCHES_NEWINST = 'oldInst' + GITBRANCHES_DAE = 'newInst-daeMode' + GITBRANCHES_NEWBACKEND_DAE = 'newBackend-daeMode' + GITBRANCHES_CPP = 'v1.19-cpp v1.20-cpp v1.21-cpp v1.22-cpp v1.23-cpp v1.24-cpp v1.25-cpp cpp v1.26-cpp v1.27-cpp' + GITBRANCHES_WASM_JIT = 'wasm-jit' + GITBRANCHES_SPECIAL = 'master wasm-jit newInst-newBackend' + PYTHONIOENCODING = 'utf-8' + IDA_EMAIL = credentials('IDA email') + // A secret file holding one pgpass line; libpq reads the password from + // it, so it never reaches a command line or the build log. It is bound + // per stage because writing the file needs the workspace of a node, + // which the pipeline as a whole does not have (agent none). + PGPASSFILE = credentials('omdb-pgpass') + } + steps { + //sshagent (credentials: ['Hudson-SSH-Key']) { + // createInitialHistoryFilesOnRemote() + //} + sh 'rm -rf *.html history' + sh ''' + if ! test -d OpenModelica; then + git clone https://openmodelica.org/git-readonly/OpenModelica.git + fi + cd OpenModelica + git fetch + ''' + sh './clean-empty-omcversion-dates.py' + + sh "./all-reports.py --email --omcgitdir=OpenModelica ${env.GITBRANCHES} ${env.GITBRANCHES_FMI} ${env.GITBRANCHES_NEWINST} ${env.GITBRANCHES_DAE} ${env.GITBRANCHES_NEWBACKEND_DAE} ${env.GITBRANCHES_CPP} ${env.GITBRANCHES_WASM_JIT} conversion heavy_tests generateSymbolicJacobian gbode cvode ida" + sh "./all-plots.py ${env.GITBRANCHES} ${env.GITBRANCHES_FMI} ${env.GITBRANCHES_NEWINST} ${env.GITBRANCHES_DAE} ${env.GITBRANCHES_NEWBACKEND_DAE} ${env.GITBRANCHES_CPP} ${env.GITBRANCHES_WASM_JIT} conversion heavy_tests generateSymbolicJacobian gbode cvode ida" + + sh "./report.py --branches='${env.GITBRANCHES} ${env.GITBRANCHES_WASM_JIT}' configs/conf.json configs/conf-old.json configs/conf-nonstandard.json" + sh 'mv overview.html overview-combined.html' + sh "./report.py --branches='${env.GITBRANCHES} ${env.GITBRANCHES_WASM_JIT}' configs/conf-old.json" + sh "mv overview.html overview-old-libs.html" + sh "./report.py --branches='${env.GITBRANCHES} ${env.GITBRANCHES_WASM_JIT}' configs/conf-nonstandard.json" + sh "mv overview.html overview-nonstandard-libs.html" + sh "./report.py --branches='${env.GITBRANCHES_SPECIAL} conversion' configs/conf.json" + sh "mv overview.html overview-special-jobs.html" + + sh "./report.py --branches='generateSymbolicJacobian' configs/conf.json" + sh "mv overview.html overview-generateSymbolicJacobian.html" + + sh "./report.py --branches='heavy_tests' configs/heavy_tests.json" + sh "mv overview.html overview-heavy_tests.html" + + sh "./report.py --branches='${env.GITBRANCHES_NEWINST}' configs/conf.json" + sh "mv overview.html overview-oldinst.html" + sh "./report.py --branches='${env.GITBRANCHES_NEWINST}' configs/conf.json configs/conf-old.json configs/conf-nonstandard.json" + sh "mv overview.html overview-combined-oldinst.html" + sh "./report.py --branches='${env.GITBRANCHES_NEWINST}' configs/conf-old.json" + sh "mv overview.html overview-old-libs-oldinst.html" + sh "./report.py --branches='${env.GITBRANCHES_NEWINST}' configs/conf-nonstandard.json" + sh "mv overview.html overview-nonstandard-libs-oldinst.html" + + sh "./report.py --branches='${env.GITBRANCHES_FMI}' configs/conf.json" + sh "mv overview.html overview-fmi.html" + sh "./report.py --branches='${env.GITBRANCHES_FMI}' configs/conf.json configs/conf-old.json configs/conf-nonstandard.json" + sh "mv overview.html overview-combined-fmi.html" + sh "./report.py --branches='${env.GITBRANCHES_FMI}' configs/conf-old.json" + sh "mv overview.html overview-old-libs-fmi.html" + sh "./report.py --branches='${env.GITBRANCHES_FMI}' configs/conf-nonstandard.json" + sh "mv overview.html overview-nonstandard-libs-fmi.html" + + sh "./report.py --branches='${env.GITBRANCHES_DAE}' configs/conf.json" + sh "mv overview.html overview-dae.html" + sh "./report.py --branches='${env.GITBRANCHES_DAE}' configs/conf.json configs/conf-old.json configs/conf-nonstandard.json" + sh "mv overview.html overview-combined-dae.html" + sh "./report.py --branches='${env.GITBRANCHES_DAE}' configs/conf-old.json" + sh "mv overview.html overview-old-libs-dae.html" + sh "./report.py --branches='${env.GITBRANCHES_DAE}' configs/conf-nonstandard.json" + sh "mv overview.html overview-nonstandard-libs-dae.html" + + sh "./report.py --branches='${env.GITBRANCHES_NEWBACKEND_DAE}' configs/conf.json" + sh "mv overview.html overview-newbackend-dae.html" + sh "./report.py --branches='${env.GITBRANCHES_NEWBACKEND_DAE}' configs/conf.json configs/conf-old.json configs/conf-nonstandard.json" + sh "mv overview.html overview-combined-newbackend-dae.html" + sh "./report.py --branches='${env.GITBRANCHES_NEWBACKEND_DAE}' configs/conf-old.json" + sh "mv overview.html overview-old-libs-newbackend-dae.html" + sh "./report.py --branches='${env.GITBRANCHES_NEWBACKEND_DAE}' configs/conf-nonstandard.json" + sh "mv overview.html overview-nonstandard-libs-newbackend-dae.html" + + sh "./report.py --branches='cvode master' configs/conf.json" + sh "mv overview.html overview-cvode.html" + + sh "./report.py --branches='gbode master' configs/conf.json" + sh "mv overview.html overview-gbode.html" + + sh "./report.py --branches='ida master' configs/conf.json" + sh "mv overview.html overview-ida.html" + + sh "./report.py --branches='wasm-jit master' configs/conf.json" + sh "mv overview.html overview-wasm-jit.html" + + sh "./report.py --branches='${env.GITBRANCHES_CPP}' configs/conf.json" + sh "mv overview.html overview-c++.html" + + sh "./report.py --branches='${env.GITBRANCHES}' configs/conf.json" + + sh 'date' + sh 'find overview*.html history -type f | wc -l' + sh 'find overview*.html history' + + sshPublisher(publishers: [sshPublisherDesc(configName: 'LibraryTestingReports', transfers: [sshTransfer(sourceFiles: 'overview*.html,history/**')])]) + } + } + + stage('drop stale pull request tables') { + agent { + dockerfile { + label 'linux' + dir '.CI/build-dep' + customWorkspace 'ws/OpenModelicaLibraryTestingReport' + } + } + when { + beforeAgent true + expression { params.drop_stale_pull_request_tables } + } + environment { + PGPASSFILE = credentials('omdb-pgpass') + } + steps { + sh './drop-pr-tables.py --yes' + } + } + + /* A pull request is not reported on by the stage above: that one compares a + * branch against its own previous run, which a pull request has none of, and + * regenerating every overview for a job that tested one thing would take + * longer than the report itself. */ + stage('pull request report') { + agent { + dockerfile { + label 'linux' + dir '.CI/build-dep' + customWorkspace 'ws/OpenModelicaLibraryTestingReport' + } + } + when { + beforeAgent true + expression { params.pull_request.trim() } + } + environment { + PYTHONIOENCODING = 'utf-8' + PGPASSFILE = credentials('omdb-pgpass') + } + steps { + script { + if (!(params.pull_request.trim() ==~ /[0-9]+/)) { + error "pull_request is a pull request number; got '${params.pull_request}'" + } + } + sh 'rm -rf history' + sh "./pr-report.py '${params.pull_request.trim()}' --baseline='${params.pull_request_baseline.trim()}'" + // The summary to comment on the pull request with, in the build log + // until there is a token to post it with. + sh 'cat history/pr-*/00_comment.md' + sshPublisher(publishers: [sshPublisherDesc(configName: 'LibraryTestingReports', transfers: [sshTransfer(sourceFiles: 'history/**')])]) + } + } } } def omsimulatorHash() { - return '1552061d5fe27ecfa51f5289909717192b102263' + return 'master' } def installLibraries(boolean removePackageOrder, boolean conversionScript, name, String omhomeTestedOMC) { sh "rm -rf '${env.HOME}/saved_omc/libraries/.openmodelica/libraries'" @@ -480,26 +655,194 @@ def installLibraries(boolean removePackageOrder, boolean conversionScript, name, sh """ cd '${WORKSPACE}/OpenModelicaLibraryTesting' OPENMODELICAHOME="${omhomeTestedOMC}" ./conversionscript.py --diff --allowErrorsInDiff '${env.HOME}/saved_omc/libraries/.openmodelica/libraries' - scp converted-libraries/.openmodelica/libraries/*.diff 'libraries.openmodelica.org:/var/www/branches/${name}' + scp converted-libraries/.openmodelica/libraries/*.diff 'libraries.openmodelica.org:/var/www/libraries.openmodelica.org/branches/${name}' """ return "${WORKSPACE}/OpenModelicaLibraryTesting/converted-libraries" } else { return "${env.HOME}/saved_omc/libraries" } } -def test(branch, name, extraFlags, omsHash, sshConfig, omcompiler, extrasimflags, boolean removePackageOrder, boolean conversionScript) { + + +def createInitialHistoryFilesOnRemote() { + sh """ +HISTORY_DIRECTORY='/var/www/libraries.openmodelica.org/branches/history/' +SEPARATOR='/' +HISTORY_FILE='00_history.html' + +for b in ${env.GITBRANCHES} ${env.GITBRANCHES_FMI} ${env.GITBRANCHES_NEWINST} ${env.GITBRANCHES_DAE} ${env.GITBRANCHES_CPP} ${env.GITBRANCHES_WASM_JIT}; do + BRANCH=`echo \${b} | cut -d '/' -f2` + FILE="\${HISTORY_DIRECTORY}\${BRANCH}\${SEPARATOR}\${HISTORY_FILE}" +ssh hudson@build.openmodelica.org << EOF +if [ ! -f \${FILE} ]; then + echo "Creating initial history: library.openmodelica.org: \${FILE}" + mkdir -p \${HISTORY_DIRECTORY}\${BRANCH} + touch \${FILE} +fi +EOF +done +""" +} + +/** + * `docker run` flags capping the container's cgroup at 90% of the node's RAM. test.py limits one + * test at a time, not the sum of the parallel ones. --memory-swap has to repeat the limit; docker + * reads 0 as "unset" and then allows swapping. + */ +def memoryLimitArgs() { + def mb = sh(script: '''awk '/^MemTotal:/ { print int($2 / 1024 * 0.85) }' /proc/meminfo''', + returnStdout: true).trim() + echo "Test container memory limit: ${mb} MB, no swap" + return "--memory=${mb}m --memory-swap=${mb}m" +} + +/** + * Runs `body` with the environment of the shared Rust compile cache of the OpenModelica job + * (OpenModelica/.CI/sccache/), which builds the same commits first. Hitting its keys needs the + * same rust settings (opt-level 2, -DRUST_OMC_THREADS=4 at the call site), the same toolchain + * (hence the same docker image) and SCCACHE_BASEDIRS set to the checkout directory, which is a + * subdirectory of the workspace here but the workspace itself there. + * + * The server is started by sccachePreamble() instead, since a build in a container cannot use + * one started outside it. + */ +def withSccache(Closure body) { + withCredentials([string(credentialsId: 'sccache-ci-secret-key', + variable: 'AWS_SECRET_ACCESS_KEY')]) { + withEnv(['RUSTC_WRAPPER=sccache', + 'SCCACHE_BUCKET=omc-sccache', + 'SCCACHE_ENDPOINT=https://sccache.openmodelica.org', + 'SCCACHE_REGION=auto', + 'SCCACHE_S3_USE_SSL=true', + 'AWS_ACCESS_KEY_ID=sccache-ci', + 'CARGO_INCREMENTAL=0', + 'CARGO_PROFILE_RELEASE_OPT_LEVEL=2', + "SCCACHE_BASEDIRS=${env.WORKSPACE}/OpenModelica"]) { + body() + } + } +} + +/** + * Prepended to a build running under withSccache: starts a server with the current environment + * (a running one keeps the one it was started with), or drops RUSTC_WRAPPER if there is no sccache. + */ +def sccachePreamble() { + return ''' + if command -v sccache > /dev/null; then + log="`mktemp`" + sccache --stop-server > /dev/null 2>&1 || true + SCCACHE_ERROR_LOG="$log" SCCACHE_LOG=warn sccache --start-server + sccache --show-stats + if grep -qiE "storage (write )?check failed|read-only storage|cache storage failed" "$log"; then + echo "WARNING: the sccache S3 backend is unusable; building without a shared cache:" >&2 + cat "$log" >&2 + fi + rm -f "$log" + else + echo "sccache was not found; building without a compile cache" + unset RUSTC_WRAPPER + fi + ''' +} + +/** + * Launches the test.py script with the given options. + * + * @param branch: OpenModelica branch to test. Will checkout the branch and build omc from it. + * @param name: Unique name of the library test. Passed to test.py via flag `--branch`. + * Also used for stashing omc and uploading results to https://test.openmodelica.org. + * @param extraFlags: Additional compiler flags passed to test.py via flag `--extraflags`. + * @param omsHash: OMSimulator SHA. + * @param omcompiler: Checkout old OMCompiler submodule. Should be `false` nowadays. + * @param extrasimflags: Additional simulation flags passed to test.py via flag `--extrasimflags`. + * @param testFlags: Additional flags passed to test.py verbatim, e.g. `--nobuildmodel`. + * @param removePackageOrder: Passed to `installLibraries`. + * @param conversionScript: Passed to `installLibraries`. + * @param jobs: The number of tests/jobs to launch in parallel. + * By default this is set to `0` which means launch as many tests as there are available + * physical cpus on the machine'. + * @param libs_config_file: The config file to be used for testing. + * This file specifies which libraries to test and what options to use for them. + * @param cmakeFlags: Target-specific cmake flags, e.g. `-DOM_OMC_ENABLE_RUST=ON`. If non-empty, omc is + * built with cmake instead of autotools; the shared release flags are added here. + * @param dockerfile: Directory with a Dockerfile, relative to the testing repository, e.g. + * `.CI/wasm-jit`. If non-empty, the omc build and test.py run in that image + * instead of on the node; everything using the node's own omc stays outside. + */ +/* The FMI simulators a job runs, from the parameters that used to start one job each. + * Both ticked is one job that builds every FMU once and simulates it with both, + * filling -fmi and -fmi-fmpy exactly as the two jobs did. */ +def fmiSimulators(boolean omsimulator, boolean fmpy) { + def simulators = [] + if (omsimulator) simulators << 'OMSimulator' + if (fmpy) simulators << 'fmpy' + return simulators +} + +def runRegressiontest(branch, name, extraFlags, omsHash, omcompiler, extrasimflags, testFlags, boolean removePackageOrder, boolean conversionScript, int jobs=0, libs_config_file = 'configs/conf.json', cmakeFlags = '', dockerfile = '', fmiSimulators = null) { sh ''' find /tmp -name "*openmodelica.hudson*" -exec rm {} ";" || true - mkdir -p ~/TEST_LIBS_BACKUP - find ~/TEST_LIBS_BACKUP -name sqlite3.*.db -mtime +7 -exec rm {} ";" if test -z "$WORKSPACE"; then echo "Odd workspace" exit 1 fi ''' - FMI_TESTING_FLAG="" - if (omsHash) { + + // Checked out before the omc build rather than just before test.py: it provides the Dockerfile. + sh """ + if test ! -d OpenModelicaLibraryTesting; then + git clone --recursive https://openmodelica.org/git-readonly/OpenModelicaLibraryTesting.git OpenModelicaLibraryTesting + fi + cd OpenModelicaLibraryTesting + git fetch + git reset --hard origin/master + """ + + def image = null + if (dockerfile) { + // Build context is the Dockerfile directory; the workspace has a directory per tested model. + sh "cp OpenModelicaLibraryTesting/requirements.txt OpenModelicaLibraryTesting/${dockerfile}/" + image = docker.build("openmodelica-library-testing:${name}", "--pull OpenModelicaLibraryTesting/${dockerfile}") + } + // --init reaps the omc processes test.py orphans. ssh refuses to run for a uid it cannot look + // up, so the node's passwd entry is needed to publish the results. The home holds the cached omc + // build, the libraries (HOME during the test) and the ssh key; test.py writes a hash next to + // every reference file. rust-cargo-registry is the volume the OpenModelica job uses. + def dockerArgs = "--init" + + (image ? " ${memoryLimitArgs()}" : '') + + " -v /etc/passwd:/etc/passwd:ro -v /etc/group:/etc/group:ro" + + " -v ${env.HOME}:${env.HOME}" + + " -v /mnt/ReferenceFiles:/mnt/ReferenceFiles" + + " --mount type=volume,source=rust-cargo-registry,target=/opt/rust/cargo/registry" + // Only in a container is the cgroup this job's own; a breach kills the greediest omc, which need + // not be the model that caused it. + def cgroupReport = image ? """ + cat /sys/fs/cgroup/memory.max || true + trap 'cat /sys/fs/cgroup/memory.peak /sys/fs/cgroup/memory.events || true' EXIT + """ : '' + // Jenkins exports the node's environment into the container, hiding the image's. + def dockerEnv = ['PATH+VENV=/opt/libtest-venv/bin', + 'PATH+CARGO=/opt/rust/cargo/bin', + 'CARGO_HOME=/opt/rust/cargo', + 'RUSTUP_HOME=/opt/rust/rustup'] + // Runs a script in the image if the target uses one, on the node otherwise. + def runSh = { args -> + if (image) { + image.inside(dockerArgs) { withEnv(dockerEnv) { sh(args) } } + } else { + sh(args) + } + } + + // A job that does not say which simulators it wants gets the one its name implies. + def simulators = fmiSimulators + if (simulators == null) { + simulators = name.contains('fmpy') ? ['fmpy'] : (omsHash ? ['OMSimulator'] : []) + } + FMI_TESTING_FLAG = "" + if (simulators.contains('OMSimulator') && omsHash) { sh """ if ! test -d OMSimulator; then git clone --recursive https://openmodelica.org/git-readonly/OMSimulator.git || exit 1 @@ -510,28 +853,106 @@ def test(branch, name, extraFlags, omsHash, sshConfig, omcompiler, extrasimflags git reset --hard "${omsHash}" || exit 1 git rev-parse HEAD > .newhash - echo "OMSimulator Hash: ${omsHash}" + echo "OMSimulator Hash: ${omsHash} and commit:" + cat .newhash || true echo Old Hash: cat ~/saved_omc/OMSimulator/.githash || true if ! (cmp ~/saved_omc/OMSimulator/.githash .newhash); then - git submodule update || exit 1 - git clean -fdx || exit 1 - git submodule foreach --recursive "git fetch --tags && git reset --hard && git clean -fdxq -e /git -e /svn" || exit 1 - make config-3rdParty || exit 1 - make config-OMSimulator || exit 1 - make OMSimulator || exit 1 + git submodule sync --recursive || exit 1 + git clean -ffdx || exit 1 + git submodule update --init --recursive --force || exit 1 + git submodule foreach --recursive "git fetch --tags --force && git reset --hard && git clean -fdxq -e /git -e /svn" || exit 1 + cmake -S . -B build/ -DCMAKE_INSTALL_PREFIX=install/ + cmake --build build/ --target install || exit 1 + ./install/bin/OMSimulator --version || exit 1 mkdir -p ~/saved_omc/OMSimulator || exit 1 cp -a * ~/saved_omc/OMSimulator/ || exit 1 git rev-parse HEAD > ~/saved_omc/OMSimulator/.githash || exit 1 fi + echo OMSimulator version: + ${env.HOME}/saved_omc/OMSimulator/install/bin/OMSimulator --version """ - FMI_TESTING_FLAG="--fmi=true --fmisimulator=${env.HOME}/saved_omc/OMSimulator/install/linux/bin/OMSimulator --default=ulimitExe=50" + FMI_TESTING_FLAG = " --fmisimulator=${env.HOME}/saved_omc/OMSimulator/install/bin/OMSimulator" } + + if (simulators.contains('fmpy')) { + sh """ + # update fmpy + pip install FMPy || true + python3 -m fmpy -h || exit 1 + """ + FMI_TESTING_FLAG += " --fmisimulator='python3 -m fmpy'" + } + + if (FMI_TESTING_FLAG) { + FMI_TESTING_FLAG = "--fmi=true${FMI_TESTING_FLAG} --default=ulimitExe=50" + if (name.contains('cvode')) { + FMI_TESTING_FLAG += " --fmuType=cs" + } + } + + // A pull request is fetched from GitHub itself: refs/pull/* is not on the + // read-only mirror the rest of the job clones. The merge ref rather than the + // head one, since the question is what happens once it is merged, and it is + // checked out detached so that nothing is left behind for the next run of the + // same workspace to trip over. + def pullRequest = branch.startsWith('pr-') && branch.substring(3).isInteger() ? branch.substring(3) : '' + def checkoutRef = pullRequest ? """ + if ! git fetch --force https://github.com/OpenModelica/OpenModelica.git refs/pull/${pullRequest}/merge; then + echo "Could not fetch refs/pull/${pullRequest}/merge: either there is no such pull request, or GitHub cannot merge it into its base branch." + exit 1 + fi + git checkout -f --detach FETCH_HEAD || exit 1 + git fetch --tags --force || exit 1 +""" : """ + git reset --hard && git checkout -f "${branch}" && (git rev-parse --verify "tags/${branch}" || (git reset --hard "origin/${branch}" && git pull)) && git fetch --tags --force || exit 1 +""" + OMCPATH = "${omcompiler ? '../' : './'}OMCompiler" + // The build runs in OMCompiler, one level below the cmake source tree. + if (cmakeFlags && omcompiler) { + error 'cmake builds need the OMCompiler directory of the OpenModelica repository (omcompiler=false)' + } + def buildOMC + if (cmakeFlags) { + buildOMC = sccachePreamble() + """ + cmake -S .. -B ../build_cmake -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="`pwd`/build" \ + -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_Fortran_COMPILER=gfortran \ + -DCMAKE_C_FLAGS=-march=native -DCMAKE_CXX_FLAGS=-march=native \ + -DOM_USE_CCACHE=OFF -DOM_ENABLE_GUI_CLIENTS=OFF -DOM_ENABLE_OMSIMULATOR=OFF \ + ${cmakeFlags} || exit 1 + if ! time cmake --build ../build_cmake --parallel 16 --target install > log 2>&1; then + cat log + exit 1 + fi + build/bin/omc --version || exit 1 + sccache --show-stats || true + """ + } else { + buildOMC = """ + autoreconf --install + ./configure --with-cppruntime --without-omc --disable-modelica3d CC=clang CXX=clang++ FC=gfortran CFLAGS='-O2 -march=native' --with-omlibrary=all --with-omniORB + time make -j16 clean + if ! time make -j9 omc > log 2>&1; then + cat log + exit 1 + fi + if ! time make -j16 runtimeCPPinstall > log 2>&1; then + cat log + if test "${name}" = "master"; then + exit 1 + else + echo "Ignoring failed C++ runtime" + fi + fi + """ + } + sh ''' FREE=`df -k --output=avail "$PWD" | tail -n1` # df -k not df -h if test "$FREE" -lt 31457280; then # 30G = 30*1024*1024k @@ -542,7 +963,7 @@ def test(branch, name, extraFlags, omsHash, sshConfig, omcompiler, extrasimflags sh 'killall omc || true' - sh """ + def checkoutAndBuild = """ if test ! -d OpenModelica; then git clone --recursive https://openmodelica.org/git-readonly/OpenModelica.git OpenModelica fi @@ -565,9 +986,9 @@ def test(branch, name, extraFlags, omsHash, sshConfig, omcompiler, extrasimflags cd ${OMCPATH} if ! test -f ~/saved_omc/${name}/.nogit; then - git reset --hard && git checkout -f "${branch}" && (git rev-parse --verify "tags/${branch}" || (git reset --hard "origin/${branch}" && git pull)) && git fetch --tags || exit 1 + ${checkoutRef} git submodule update --init --recursive --force || (rm -rf * && git reset --hard && git submodule update --init --recursive --force) || exit 1 - git submodule foreach --recursive "git fetch --tags && git reset --hard && git clean -fdxq -e /git -e /svn" || exit 1 + git submodule foreach --recursive "git fetch --tags --force && git reset --hard && git clean -fdxq -e /git -e /svn" || exit 1 git clean -fdxq || exit 1 git submodule status --recursive fi @@ -581,21 +1002,7 @@ def test(branch, name, extraFlags, omsHash, sshConfig, omcompiler, extrasimflags cat ~/saved_omc/${name}/.githash || true # TODO: Create a docker image for these things instead? if ! (cmp ~/saved_omc/${name}/.githash .newhash || test -f ~/saved_omc/${name}/.nogit); then - autoconf - ./configure --with-cppruntime --without-omc --disable-modelica3d CC=clang CXX=clang++ FC=gfortran CFLAGS='-O2 -march=native' --with-omlibrary=all --with-omniORB - time make -j16 clean - if ! time make -j9 omc > log 2>&1; then - cat log - exit 1 - fi - if ! time make -j16 runtimeCPPinstall > log 2>&1; then - cat log - if test "${name}" = "master"; then - exit 1 - else - echo "Ignoring failed C++ runtime" - fi - fi + ${buildOMC} rm -rf ~/saved_omc/${name}/ mkdir -p ~/saved_omc/${name}/ CMD="rsync -a --delete build/ \$HOME/saved_omc/${name}/" @@ -608,6 +1015,12 @@ def test(branch, name, extraFlags, omsHash, sshConfig, omcompiler, extrasimflags fi """ + if (cmakeFlags) { + withSccache { runSh(checkoutAndBuild) } + } else { + runSh(checkoutAndBuild) + } + sh """ cd OpenModelica rm -rf "`pwd`/${OMCPATH}/build/lib/omlibrary/" @@ -632,53 +1045,46 @@ def test(branch, name, extraFlags, omsHash, sshConfig, omcompiler, extrasimflags REFERENCEFILES="${WORKSPACE}/OpenModelica/testsuite/ReferenceFiles" GITREPOS="${WORKSPACE}/OpenModelica/libraries/git" PNLIBREFS="/mnt/ReferenceFiles/PNlib/ReferenceFiles" - THERMOFLUIDSTREAMREFS="/mnt/ReferenceFiles/ThermofluidStream-main/ReferenceData" - THERMOFLUIDSTREAMREFSOM="/mnt/ReferenceFiles/ThermofluidStream-OM/ReferenceData" + THERMOFLUIDSTREAMREFS="/mnt/ReferenceFiles/ThermofluidStream-main-regression/ReferenceData" + THERMOFLUIDSTREAMREFSOM="/mnt/ReferenceFiles/ThermofluidStream-OM-regression/ReferenceData" sh """ test -f "${MSLREFERENCE}/MAP-LIB_ReferenceResults/v4.0.0/README.md" || exit 1 - if test ! -d OpenModelicaLibraryTesting; then - git clone --recursive https://openmodelica.org/git-readonly/OpenModelicaLibraryTesting.git OpenModelicaLibraryTesting - fi - cd OpenModelicaLibraryTesting - git fetch - git reset --hard origin/master - - mkdir -p "/var/www/branches/${name}/" + mkdir -p "/var/www/libraries.openmodelica.org/branches/${name}/" """ - - def libraryPath = installLibraries(removePackageOrder, conversionScript, name, "${WORKSPACE}/OpenModelica/${OMCPATH}/build") - - sh "test -d '${libraryPath}/.openmodelica/libraries/Modelica trunk'" - sh ''' - cp ~/TEST_LIBS_BACKUP/sqlite3.db OpenModelicaLibraryTesting/sqlite3.db - test -s OpenModelicaLibraryTesting/sqlite3.db - ''' + def libraryPath = installLibraries(removePackageOrder, conversionScript, name, "${WORKSPACE}/OpenModelica/${OMCPATH}/build") - sh 'date' + sh "test -d '${libraryPath}/.openmodelica/libraries/Modelica trunk'" - sh """ - export OPENMODELICAHOME="${WORKSPACE}/OpenModelica/${OMCPATH}/build" - export MSLREFERENCE="${MSLREFERENCE}" - export REFERENCEFILES="${REFERENCEFILES}" - export GITREPOS="${GITREPOS}" - export PNLIBREFS="${PNLIBREFS}" - export THERMOFLUIDSTREAMREFS="${THERMOFLUIDSTREAMREFS}" - export THERMOFLUIDSTREAMREFSOM="${THERMOFLUIDSTREAMREFSOM}" - export HOME="${libraryPath}" - cd OpenModelicaLibraryTesting - # Force /usr/bin/omc as being used for generating the mos-files. Ensures consistent behavior among all tested OMC versions - stdbuf -oL -eL time python2 ./test.py --ompython_omhome=/usr ${FMI_TESTING_FLAG} --extraflags='${extraFlags}' --extrasimflags='${extrasimflags}' --branch="${name}" --output="libraries.openmodelica.org:/var/www/branches/${name}/" --libraries='${libraryPath}/.openmodelica/libraries/' configs/conf.json ${conversionScript ? "" : "configs/conf-c++.json"} ${params.OLD_LIBS ? "configs/conf-old.json configs/conf-nonstandard.json" : ""} || (killall omc ; false) || exit 1 - """ sh 'date' - sh 'rm -f OpenModelicaLibraryTesting/sqlite3.db.tmp' - sh 'ln OpenModelicaLibraryTesting/sqlite3.db OpenModelicaLibraryTesting/sqlite3.db.tmp' - sh 'cp OpenModelicaLibraryTesting/sqlite3.db ~/TEST_LIBS_BACKUP/sqlite3.db' - sh 'rm -f ~/TEST_LIBS_BACKUP/`date +sqlite3.%Y%m%d.db`' + // The password of the results database comes from a secret file, bound here + // rather than for the pipeline as a whole: writing the file needs the + // workspace of a node, and the pipeline has agent none. + withCredentials([file(credentialsId: 'omdb-pgpass', variable: 'PGPASSFILE')]) { + runSh(""" + export OPENMODELICAHOME="${WORKSPACE}/OpenModelica/${OMCPATH}/build" + export MSLREFERENCE="${MSLREFERENCE}" + export REFERENCEFILES="${REFERENCEFILES}" + export GITREPOS="${GITREPOS}" + export PNLIBREFS="${PNLIBREFS}" + export THERMOFLUIDSTREAMREFS="${THERMOFLUIDSTREAMREFS}" + export THERMOFLUIDSTREAMREFSOM="${THERMOFLUIDSTREAMREFSOM}" + export PREVIOUSHOME="${env.HOME}" + export HOME="${libraryPath}" + # we need to do some crap magic here to make sure python3 finds fmpy as we change the HOME here + # too bad if we cannot do it, just continue + ln -s -t \${HOME} \${PREVIOUSHOME}/.local .local || true - sshPublisher(publishers: [sshPublisherDesc(configName: sshConfig, transfers: [sshTransfer(removePrefix: 'OpenModelicaLibraryTesting', sourceFiles: 'OpenModelicaLibraryTesting/sqlite3.db', failOnError: true)])]) + ${cgroupReport} + cd OpenModelicaLibraryTesting + # Force /usr/bin/omc as being used for generating the mos-files. Ensures consistent behavior among all tested OMC versions + stdbuf -oL -eL time ./test.py --ompython_omhome=/usr ${FMI_TESTING_FLAG} --extraflags='${extraFlags}' --extrasimflags='${extrasimflags}' ${testFlags} --branch="${name}" --output="libraries.openmodelica.org:/var/www/libraries.openmodelica.org/branches/${name}/" --libraries='${libraryPath}/.openmodelica/libraries/' --jobs=${jobs} ${libs_config_file} ${params.OLDLIBS ? "configs/conf-old.json configs/conf-nonstandard.json" : ""} || (killall omc ; false) || exit 1 + """) + sh 'date' + sh "cd OpenModelicaLibraryTesting/ && ./clean-empty-omcversion-dates.py" + } } diff --git a/.CI/build-dep/Dockerfile b/.CI/build-dep/Dockerfile index 02de218..5f329b7 100644 --- a/.CI/build-dep/Dockerfile +++ b/.CI/build-dep/Dockerfile @@ -1,3 +1,7 @@ -FROM docker.openmodelica.org/build-deps:v1.13 +# The image the OpenModelica repository builds against, see its .CI/common.groovy +FROM docker.openmodelica.org/build-deps:ubuntu-22.04 -RUN pip3 install matplotlib +# -y because a base image that does not assume it turns the build into a prompt, +# and python3-pip because the line below needs it and the base may not carry it. +RUN apt-get update && apt-get install -y libxml2 libxslt1.1 libxml2-dev libxslt1-dev python3-pip +RUN pip3 install matplotlib FMPy psycopg2-binary diff --git a/.CI/installLibraries.mos b/.CI/installLibraries.mos index e9004a6..4871669 100644 --- a/.CI/installLibraries.mos +++ b/.CI/installLibraries.mos @@ -1,29 +1,124 @@ +// adrpo: DO NOT REMOVE ANY LIBS FROM HERE BEFORE REMOVING THEM FROM configs/conf* +// usually the newest version will be loaded instead if you remove them! +// SO: first remove them from configs/conf* then remove them from here. updatePackageIndex(); if not installPackage(Modelica, "3.1.0", exactMatch=true) then print("Modelica 3.1.0 " + getErrorString() + "\n"); exit(1); end if; -for v in {"3.2.1","3.2.2","3.2.3","4.0.0","trunk"} loop +for v in {"3.2.1","3.2.2","3.2.3","4.0.0","4.1.0","trunk"} loop if not installPackage(ModelicaTest, v, exactMatch=true) then print("ModelicaTest " + v + " " + getErrorString() + "\n"); exit(1); end if; end for; -for v in {"1.6", "3.0.0", "maint.7.0.x", "8.0.0", "master"} loop +for v in {"1.6", "3.0.0", "maint.7.0.x", "maint.8.1.x", "maint.9.1.x", "maint.10.0.x", "maint.11.x", "maint.12.x", "master"} loop if not installPackage(Buildings, v) then print("Buildings " + v + " " + getErrorString() + "\n"); exit(1); end if; end for; -for lib in {"AdvancedNoise", "AixLib", "BioChem", "BuildingSystems", "BuildSysPro", "Chemical", "ClaRa_Obsolete", "ClaRa", "ConPNlib", "ElectricalEnergyStorage", "ExternData", "ExternalMemoryLib", "FastBuildings", "FCSys", "HanserModelica", "HelmholtzMedia", "IBPSA", "IdealizedContact", "IndustrialControlSystems", "KeyWordIO", "LibRAS", "MEV", "MessagePack", "ModelicaByExample", "ModelicaTestOverdetermined", "Modelica_Synchronous", "Modelica_DeviceDrivers", "Modelica_LinearSystems2", "Modelica_Noise", "ObjectStab", "OpenHydraulics", "OpenIPSL", "PNlib", "Physiolibrary", "PlanarMechanics", "PowerGrids", "PowerSysPro", "ScalableTestGrids", "ScalableTestSuite", "SolarTherm", "Spot", "SystemDynamics", "TAeZoSysPro", "TAeZoSysPro_testsuite", "ThermalSeparation", "ThermoPower", "ThermoSysPro", "VehicleInterfaces"} loop - if not installPackage(stringTypeName(lib), "master") then - print(lib + " " + getErrorString() + "\n"); - exit(1); - end if; -end for; - -for l in {{"AES", "main"}, {"Annex60", ""}, {"ExtendedPetriNets", ""}, {"iPSL", ""}, {"PhotoVoltaics", ""}, {"PhotoVoltaics_TGM", ""}, {"PowerSystems","master"}, {"PowerSystems",""}, {"SiemensPower",""}, {"SiemensPower","OMCtest"}, {"Modelica_StateGraph2", ""}, {"OpenIPSL", ""}, {"Physiomodel", ""}, {"ThermofluidStream", "main"}, {"ThermofluidStream", "0.2.0-OM_adaptions"}, {"TILMedia", "ClaRa"}} loop +for l in { + {"AdvancedNoise", "master"}, + {"AES", "main"}, + {"AixLib", "main"}, + {"Annex60", ""}, + {"BidiDCDC", ""}, + {"BioChem", "master"}, + {"BLDC", ""}, + {"BoundaryInitBlocks", "master"}, + {"BuildingSystems", "master"}, + {"BuildSysPro", "master"}, + {"BusinessSimulation", ""}, + {"BusinessSimulation", "main"}, + {"Chemical", "1.4.1"}, + {"Chemical", "master"}, + {"ClaRa", ""}, + {"ClaRa", "main"}, + {"ConPNlib", "master"}, + {"Credibility", ""}, + {"DeltaRobot", ""}, + {"DroneLibrary", ""}, + {"Dynawo", ""}, + {"Dynawo", "master"}, + {"EHPTexamples", ""}, + {"EHPTexamples", "main"}, + {"EHPTlib", ""}, + {"EHPTlib", "main"}, + {"ElectricalEnergyStorage", "master"}, + {"ExtendedPetriNets", ""}, + {"ExternalMedia",""}, + {"ExternalMemoryLib", "master"}, + {"ExternData", "master"}, + {"FastBuildings", "master"}, + {"FCSys", "master"}, + {"FluxTubesExtension","main"}, + {"Greenhouses", "master"}, + {"HanserModelica", "master"}, + {"HelmholtzMedia", "main"}, + {"IBPSA", "master"}, + {"IdealizedContact", "master"}, + {"IDEAS", ""}, + {"IDEAS", "master"}, + {"IndustrialControlSystems", "master"}, + {"iPSL", ""}, + {"KeyWordIO", "master"}, + {"LargeTestSuite", "master"}, + {"LEPSE", "main"}, + {"LibRAS", "master"}, + {"MagLev", ""}, + {"MessagePack", "master"}, + {"MEV", "master"}, + {"Modelica_DeviceDrivers", "master"}, + {"Modelica_LinearSystems2", "dev"}, + {"Modelica_LinearSystems2", "master"}, + {"Modelica_Noise", "master"}, + {"Modelica_StateGraph2", ""}, + {"Modelica_Synchronous", "master"}, + {"ModelicaByExample", "master"}, + {"ModelicaTestOverdetermined", "master"}, + {"NeuralNetwork", "main"}, + {"ObjectStab", "master"}, + {"OpenHydraulics", "main"}, + {"OpenIMDML", "main"}, + {"OpenIPSL", ""}, + {"OpenIPSL", "2.0.0"}, + {"OpenIPSL", "master"}, + {"PhotoVoltaics_TGM", ""}, + {"PhotoVoltaics", ""}, + {"Physiolibrary", "master"}, + {"Physiomodel", ""}, + {"PlanarMechanics", "master"}, + {"PNlib", "master"}, + {"PowerGrids",""}, + {"PowerGrids","develop"}, + {"PowerSysPro", "master"}, + {"PowerSystems",""}, + {"PowerSystems","master"}, + {"ScalableTestGrids", "master"}, + {"ScalableTestSuite", "master"}, + {"SiemensPower",""}, + {"SiemensPower","OMCtest"}, + {"SOFCPoliMi","master"}, + {"SolarTherm", "master"}, + {"Spot", "master"}, + {"StewartPlatform",""}, + {"SystemDynamics", "master"}, + {"TAeZoSysPro_testsuite", "master"}, + {"TAeZoSysPro", "master"}, + {"ThermalSeparation", "master"}, + {"ThermofluidStream", ""}, + {"ThermofluidStream", "main"}, + {"ThermoPower", "master"}, + {"ThermoPower", "4.0.0-dev"}, + {"ThermoSysPro", "master"}, + {"TILMedia", "main"}, + {"TILMedia_AddOn_Tests", "main"}, + {"TransiEnt", "master"}, + {"VehicleInterfaces", "master"}, + {"URDFModelica", "main"} +} loop if not installPackage(stringTypeName(l[1]), l[2]) then print(l[1] + " " + l[2] + getErrorString() + "\n"); exit(1); diff --git a/.CI/wasm-jit/Dockerfile b/.CI/wasm-jit/Dockerfile new file mode 100644 index 0000000..30ab938 --- /dev/null +++ b/.CI/wasm-jit/Dockerfile @@ -0,0 +1,42 @@ +# Image for the targets needing the Rust omc (wasm-jit); the nodes run an Ubuntu +# too old to build it. Built by .CI/Jenkinsfile, with this directory as the +# context (the job copies requirements.txt into it). +FROM docker.openmodelica.org/build-deps:ubuntu-26.04-rust + +# rsync/ssh publish the results; time and killall are used around test.py. +# libcomedi-dev libx11-dev is used by Modelica_DeviceDrivers +# Python 3.8 and 3.12 are used by Buildings +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get update \ + && apt-get install -qy software-properties-common \ + && add-apt-repository -y ppa:deadsnakes/ppa \ + && apt-get install -qy --no-install-recommends openssh-client psmisc rsync time \ + && apt-get install -qy --no-install-recommends libcomedi-dev libx11-dev \ + && apt-get install -qy python3.8 python3.12 libpython3.8-dev libpython3.12-dev \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# On PATH rather than named explicitly: test.py starts testmodel.py through its +# `#!/usr/bin/env python3` shebang. +COPY requirements.txt /tmp/requirements.txt +RUN python3 -m venv /opt/libtest-venv \ + && /opt/libtest-venv/bin/pip install --no-cache-dir -r /tmp/requirements.txt \ + # Python dependencies of library install scripts (e.g. Buildings EnergyPlus) + && /opt/libtest-venv/bin/pip install --no-cache-dir jinja2 \ + && rm /tmp/requirements.txt +ENV PATH=/opt/libtest-venv/bin:$PATH + +# The omc for --ompython_omhome (the tested one is built in the workspace). The +# base image points apt at the nightly component; use stable instead. +# OMC_VERSION pins a version, empty takes the newest stable one. +ARG OMC_VERSION= +RUN export DEBIAN_FRONTEND=noninteractive \ + && . /etc/os-release \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/openmodelica-keyring.gpg]" \ + "https://build.openmodelica.org/apt ${VERSION_CODENAME} stable" \ + > /etc/apt/sources.list.d/openmodelica.list \ + && apt-get update \ + && apt-get install -qy omc${OMC_VERSION:+=${OMC_VERSION}} \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && omc --version diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..46de937 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,27 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu +{ + "name": "Ubuntu", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/base:noble", + + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "ghcr.io/devcontainers/features/python:1": { + "installTools": true, + "version": "latest" + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "bash .devcontainer/install.sh && omc .devcontainer/install.mos && pip install -r requirements.txt" + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.devcontainer/install.mos b/.devcontainer/install.mos new file mode 100644 index 0000000..aa626ee --- /dev/null +++ b/.devcontainer/install.mos @@ -0,0 +1,2 @@ +updatePackageIndex(); getErrorString(); +installPackage(Modelica, version="4.0.0", exactMatch=true); getErrorString(); diff --git a/.devcontainer/install.sh b/.devcontainer/install.sh new file mode 100644 index 0000000..65472c3 --- /dev/null +++ b/.devcontainer/install.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +sudo apt-get update +sudo apt-get install ca-certificates curl gnupg -qy +sudo curl -fsSL http://build.openmodelica.org/apt/openmodelica.asc | \ + sudo gpg --dearmor -o /usr/share/keyrings/openmodelica-keyring.gpg + +echo "deb [arch=amd64 signed-by=/usr/share/keyrings/openmodelica-keyring.gpg] \ + https://build.openmodelica.org/apt \ + noble \ + stable" | sudo tee /etc/apt/sources.list.d/openmodelica.list + +sudo apt update +sudo apt install --no-install-recommends omc -qy diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..061c8ca --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.mo linguist-language=Modelica +*.bmo linguist-language=Modelica +*.html.tpl linguist-language=HTML diff --git a/.github/scripts/archiveResults.py b/.github/scripts/archiveResults.py new file mode 100644 index 0000000..ed1e68b --- /dev/null +++ b/.github/scripts/archiveResults.py @@ -0,0 +1,47 @@ +#!/usr/bin/python3 + +import os +import shutil +import sys + +def copyFiles(libraryName, libVersion, branchOM, targetDir): + """ + Zip HTML files and save in target directory. + + Arguments + libraryName -- Name of Modelica library. + libVersion -- Branch or version of Modelica library. + branchOM -- Branch or version of OpenModelica. + targetDir -- Target directory. + """ + + libNameBranch = f"{libraryName}_{libVersion}" + + shutil.copytree("files", + os.path.join(targetDir, branchOM, libNameBranch, "files"), + dirs_exist_ok = True) + + shutil.copy2("overview.html", + os.path.join(targetDir, "index.html")) + + # Copy library overview + shutil.copy2(f"{libNameBranch}.html", + os.path.join(targetDir, branchOM, libNameBranch, f"{libNameBranch}.html")) + + # Copy dygraph script + shutil.copy2(os.path.join(".github", "scripts", "dygraph-combined.js"), + os.path.join(targetDir, branchOM, libNameBranch, "files", "dygraph-combined.js")) + +if len(sys.argv) != 5: + raise Exception("Wrong number of input arguments.\nUsage:\narchieveResults.py libraryName libVersion branchOM omLibTestingDir zipFile") + +libraryName = sys.argv[1] +libVersion = sys.argv[2] +branchOM = sys.argv[3] +targetDir = sys.argv[4] + +# If running inside a pull request +if libVersion.endswith('/merge'): + libVersion = 'dev-pr-' + libVersion.replace('/merge', '') + +copyFiles(libraryName, libVersion, branchOM, targetDir) diff --git a/.github/scripts/dygraph-combined.js b/.github/scripts/dygraph-combined.js new file mode 100644 index 0000000..c76aa1f --- /dev/null +++ b/.github/scripts/dygraph-combined.js @@ -0,0 +1,7234 @@ +/*! @license Copyright 2011 Dan Vanderkam (danvdk@gmail.com) MIT-licensed (http://opensource.org/licenses/MIT) */ +Date.ext = {}; +Date.ext.util = {}; +Date.ext.util.xPad = function(a, c, b) { + if (typeof(b) == "undefined") { + b = 10 + } + for (; parseInt(a, 10) < b && b > 1; b /= 10) { + a = c.toString() + a + } + return a.toString() +}; +Date.prototype.locale = "en-GB"; +if (document.getElementsByTagName("html") && document.getElementsByTagName("html")[0].lang) { + Date.prototype.locale = document.getElementsByTagName("html")[0].lang +} +Date.ext.locales = {}; +Date.ext.locales.en = { + a: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], + A: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], + b: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + B: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], + c: "%a %d %b %Y %T %Z", + p: ["AM", "PM"], + P: ["am", "pm"], + x: "%d/%m/%y", + X: "%T" +}; +Date.ext.locales["en-US"] = Date.ext.locales.en; +Date.ext.locales["en-US"].c = "%a %d %b %Y %r %Z"; +Date.ext.locales["en-US"].x = "%D"; +Date.ext.locales["en-US"].X = "%r"; +Date.ext.locales["en-GB"] = Date.ext.locales.en; +Date.ext.locales["en-AU"] = Date.ext.locales["en-GB"]; +Date.ext.formats = { + a: function(a) { + return Date.ext.locales[a.locale].a[a.getDay()] + }, + A: function(a) { + return Date.ext.locales[a.locale].A[a.getDay()] + }, + b: function(a) { + return Date.ext.locales[a.locale].b[a.getMonth()] + }, + B: function(a) { + return Date.ext.locales[a.locale].B[a.getMonth()] + }, + c: "toLocaleString", + C: function(a) { + return Date.ext.util.xPad(parseInt(a.getFullYear() / 100, 10), 0) + }, + d: ["getDate", "0"], + e: ["getDate", " "], + g: function(a) { + return Date.ext.util.xPad(parseInt(Date.ext.util.G(a) / 100, 10), 0) + }, + G: function(c) { + var e = c.getFullYear(); + var b = parseInt(Date.ext.formats.V(c), 10); + var a = parseInt(Date.ext.formats.W(c), 10); + if (a > b) { + e++ + } else { + if (a === 0 && b >= 52) { + e-- + } + } + return e + }, + H: ["getHours", "0"], + I: function(b) { + var a = b.getHours() % 12; + return Date.ext.util.xPad(a === 0 ? 12 : a, 0) + }, + j: function(c) { + var a = c - new Date("" + c.getFullYear() + "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/1/1 GMT"); + a += c.getTimezoneOffset() * 60000; + var b = parseInt(a / 60000 / 60 / 24, 10) + 1; + return Date.ext.util.xPad(b, 0, 100) + }, + m: function(a) { + return Date.ext.util.xPad(a.getMonth() + 1, 0) + }, + M: ["getMinutes", "0"], + p: function(a) { + return Date.ext.locales[a.locale].p[a.getHours() >= 12 ? 1 : 0] + }, + P: function(a) { + return Date.ext.locales[a.locale].P[a.getHours() >= 12 ? 1 : 0] + }, + S: ["getSeconds", "0"], + u: function(a) { + var b = a.getDay(); + return b === 0 ? 7 : b + }, + U: function(e) { + var a = parseInt(Date.ext.formats.j(e), 10); + var c = 6 - e.getDay(); + var b = parseInt((a + c) / 7, 10); + return Date.ext.util.xPad(b, 0) + }, + V: function(e) { + var c = parseInt(Date.ext.formats.W(e), 10); + var a = (new Date("" + e.getFullYear() + "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/1/1")).getDay(); + var b = c + (a > 4 || a <= 1 ? 0 : 1); + if (b == 53 && (new Date("" + e.getFullYear() + "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/12/31")).getDay() < 4) { + b = 1 + } else { + if (b === 0) { + b = Date.ext.formats.V(new Date("" + (e.getFullYear() - 1) + "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/12/31")) + } + } + return Date.ext.util.xPad(b, 0) + }, + w: "getDay", + W: function(e) { + var a = parseInt(Date.ext.formats.j(e), 10); + var c = 7 - Date.ext.formats.u(e); + var b = parseInt((a + c) / 7, 10); + return Date.ext.util.xPad(b, 0, 10) + }, + y: function(a) { + return Date.ext.util.xPad(a.getFullYear() % 100, 0) + }, + Y: "getFullYear", + z: function(c) { + var b = c.getTimezoneOffset(); + var a = Date.ext.util.xPad(parseInt(Math.abs(b / 60), 10), 0); + var e = Date.ext.util.xPad(b % 60, 0); + return (b > 0 ? "-" : "+") + a + e + }, + Z: function(a) { + return a.toString().replace(/^.*\(([^)]+)\)$/, "$1") + }, + "%": function(a) { + return "%" + } +}; +Date.ext.aggregates = { + c: "locale", + D: "%m/%d/%y", + h: "%b", + n: "\n", + r: "%I:%M:%S %p", + R: "%H:%M", + t: "\t", + T: "%H:%M:%S", + x: "locale", + X: "locale" +}; +Date.ext.aggregates.z = Date.ext.formats.z(new Date()); +Date.ext.aggregates.Z = Date.ext.formats.Z(new Date()); +Date.ext.unsupported = {}; +Date.prototype.strftime = function(a) { + if (!(this.locale in Date.ext.locales)) { + if (this.locale.replace(/-[a-zA-Z]+$/, "") in Date.ext.locales) { + this.locale = this.locale.replace(/-[a-zA-Z]+$/, "") + } else { + this.locale = "en-GB" + } + } + var c = this; + while (a.match(/%[cDhnrRtTxXzZ]/)) { + a = a.replace(/%([cDhnrRtTxXzZ])/g, function(e, d) { + var g = Date.ext.aggregates[d]; + return (g == "locale" ? Date.ext.locales[c.locale][d] : g) + }) + } + var b = a.replace(/%([aAbBCdegGHIjmMpPSuUVwWyY%])/g, function(e, d) { + var g = Date.ext.formats[d]; + if (typeof(g) == "string") { + return c[g]() + } else { + if (typeof(g) == "function") { + return g.call(c, c) + } else { + if (typeof(g) == "object" && typeof(g[0]) == "string") { + return Date.ext.util.xPad(c[g[0]](), g[1]) + } else { + return d + } + } + } + }); + c = null; + return b +}; +"use strict"; + +function RGBColorParser(f) { + this.ok = false; + if (f.charAt(0) == "#") { + f = f.substr(1, 6) + } + f = f.replace(/ /g, ""); + f = f.toLowerCase(); + var b = { + aliceblue: "f0f8ff", + antiquewhite: "faebd7", + aqua: "00ffff", + aquamarine: "7fffd4", + azure: "f0ffff", + beige: "f5f5dc", + bisque: "ffe4c4", + black: "000000", + blanchedalmond: "ffebcd", + blue: "0000ff", + blueviolet: "8a2be2", + brown: "a52a2a", + burlywood: "deb887", + cadetblue: "5f9ea0", + chartreuse: "7fff00", + chocolate: "d2691e", + coral: "ff7f50", + cornflowerblue: "6495ed", + cornsilk: "fff8dc", + crimson: "dc143c", + cyan: "00ffff", + darkblue: "00008b", + darkcyan: "008b8b", + darkgoldenrod: "b8860b", + darkgray: "a9a9a9", + darkgreen: "006400", + darkkhaki: "bdb76b", + darkmagenta: "8b008b", + darkolivegreen: "556b2f", + darkorange: "ff8c00", + darkorchid: "9932cc", + darkred: "8b0000", + darksalmon: "e9967a", + darkseagreen: "8fbc8f", + darkslateblue: "483d8b", + darkslategray: "2f4f4f", + darkturquoise: "00ced1", + darkviolet: "9400d3", + deeppink: "ff1493", + deepskyblue: "00bfff", + dimgray: "696969", + dodgerblue: "1e90ff", + feldspar: "d19275", + firebrick: "b22222", + floralwhite: "fffaf0", + forestgreen: "228b22", + fuchsia: "ff00ff", + gainsboro: "dcdcdc", + ghostwhite: "f8f8ff", + gold: "ffd700", + goldenrod: "daa520", + gray: "808080", + green: "008000", + greenyellow: "adff2f", + honeydew: "f0fff0", + hotpink: "ff69b4", + indianred: "cd5c5c", + indigo: "4b0082", + ivory: "fffff0", + khaki: "f0e68c", + lavender: "e6e6fa", + lavenderblush: "fff0f5", + lawngreen: "7cfc00", + lemonchiffon: "fffacd", + lightblue: "add8e6", + lightcoral: "f08080", + lightcyan: "e0ffff", + lightgoldenrodyellow: "fafad2", + lightgrey: "d3d3d3", + lightgreen: "90ee90", + lightpink: "ffb6c1", + lightsalmon: "ffa07a", + lightseagreen: "20b2aa", + lightskyblue: "87cefa", + lightslateblue: "8470ff", + lightslategray: "778899", + lightsteelblue: "b0c4de", + lightyellow: "ffffe0", + lime: "00ff00", + limegreen: "32cd32", + linen: "faf0e6", + magenta: "ff00ff", + maroon: "800000", + mediumaquamarine: "66cdaa", + mediumblue: "0000cd", + mediumorchid: "ba55d3", + mediumpurple: "9370d8", + mediumseagreen: "3cb371", + mediumslateblue: "7b68ee", + mediumspringgreen: "00fa9a", + mediumturquoise: "48d1cc", + mediumvioletred: "c71585", + midnightblue: "191970", + mintcream: "f5fffa", + mistyrose: "ffe4e1", + moccasin: "ffe4b5", + navajowhite: "ffdead", + navy: "000080", + oldlace: "fdf5e6", + olive: "808000", + olivedrab: "6b8e23", + orange: "ffa500", + orangered: "ff4500", + orchid: "da70d6", + palegoldenrod: "eee8aa", + palegreen: "98fb98", + paleturquoise: "afeeee", + palevioletred: "d87093", + papayawhip: "ffefd5", + peachpuff: "ffdab9", + peru: "cd853f", + pink: "ffc0cb", + plum: "dda0dd", + powderblue: "b0e0e6", + purple: "800080", + red: "ff0000", + rosybrown: "bc8f8f", + royalblue: "4169e1", + saddlebrown: "8b4513", + salmon: "fa8072", + sandybrown: "f4a460", + seagreen: "2e8b57", + seashell: "fff5ee", + sienna: "a0522d", + silver: "c0c0c0", + skyblue: "87ceeb", + slateblue: "6a5acd", + slategray: "708090", + snow: "fffafa", + springgreen: "00ff7f", + steelblue: "4682b4", + tan: "d2b48c", + teal: "008080", + thistle: "d8bfd8", + tomato: "ff6347", + turquoise: "40e0d0", + violet: "ee82ee", + violetred: "d02090", + wheat: "f5deb3", + white: "ffffff", + whitesmoke: "f5f5f5", + yellow: "ffff00", + yellowgreen: "9acd32" + }; + for (var g in b) { + if (f == g) { + f = b[g] + } + } + var e = [{ + re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, + example: ["rgb(123, 234, 45)", "rgb(255,234,245)"], + process: function(i) { + return [parseInt(i[1]), parseInt(i[2]), parseInt(i[3])] + } + }, { + re: /^(\w{2})(\w{2})(\w{2})$/, + example: ["#00ff00", "336699"], + process: function(i) { + return [parseInt(i[1], 16), parseInt(i[2], 16), parseInt(i[3], 16)] + } + }, { + re: /^(\w{1})(\w{1})(\w{1})$/, + example: ["#fb0", "f0f"], + process: function(i) { + return [parseInt(i[1] + i[1], 16), parseInt(i[2] + i[2], 16), parseInt(i[3] + i[3], 16)] + } + }]; + for (var c = 0; c < e.length; c++) { + var j = e[c].re; + var a = e[c].process; + var h = j.exec(f); + if (h) { + var d = a(h); + this.r = d[0]; + this.g = d[1]; + this.b = d[2]; + this.ok = true + } + } + this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r); + this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g); + this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b); + this.toRGB = function() { + return "rgb(" + this.r + ", " + this.g + ", " + this.b + ")" + }; + this.toHex = function() { + var l = this.r.toString(16); + var k = this.g.toString(16); + var i = this.b.toString(16); + if (l.length == 1) { + l = "0" + l + } + if (k.length == 1) { + k = "0" + k + } + if (i.length == 1) { + i = "0" + i + } + return "#" + l + k + i + } +} + +function printStackTrace(b) { + b = b || { + guess: true + }; + var c = b.e || null, + e = !!b.guess; + var d = new printStackTrace.implementation(), + a = d.run(c); + return (e) ? d.guessAnonymousFunctions(a) : a +} +printStackTrace.implementation = function() {}; +printStackTrace.implementation.prototype = { + run: function(a, b) { + a = a || this.createException(); + b = b || this.mode(a); + if (b === "other") { + return this.other(arguments.callee) + } else { + return this[b](a) + } + }, + createException: function() { + try { + this.undef() + } catch (a) { + return a + } + }, + mode: function(a) { + if (a["arguments"] && a.stack) { + return "chrome" + } else { + if (typeof a.message === "string" && typeof window !== "undefined" && window.opera) { + if (!a.stacktrace) { + return "opera9" + } + if (a.message.indexOf("\n") > -1 && a.message.split("\n").length > a.stacktrace.split("\n").length) { + return "opera9" + } + if (!a.stack) { + return "opera10a" + } + if (a.stacktrace.indexOf("called from line") < 0) { + return "opera10b" + } + return "opera11" + } else { + if (a.stack) { + return "firefox" + } + } + } + return "other" + }, + instrumentFunction: function(b, d, e) { + b = b || window; + var a = b[d]; + b[d] = function c() { + e.call(this, printStackTrace().slice(4)); + return b[d]._instrumented.apply(this, arguments) + }; + b[d]._instrumented = a + }, + deinstrumentFunction: function(a, b) { + if (a[b].constructor === Function && a[b]._instrumented && a[b]._instrumented.constructor === Function) { + a[b] = a[b]._instrumented + } + }, + chrome: function(b) { + var a = (b.stack + "\n").replace(/^\S[^\(]+?[\n$]/gm, "").replace(/^\s+at\s+/gm, "").replace(/^([^\(]+?)([\n$])/gm, "{anonymous}()@$1$2").replace(/^Object.\s*\(([^\)]+)\)/gm, "{anonymous}()@$1").split("\n"); + a.pop(); + return a + }, + firefox: function(a) { + return a.stack.replace(/(?:\n@:0)?\s+$/m, "").replace(/^\(/gm, "{anonymous}(").split("\n") + }, + opera11: function(g) { + var a = "{anonymous}", + h = /^.*line (\d+), column (\d+)(?: in (.+))? in (\S+):$/; + var k = g.stacktrace.split("\n"), + l = []; + for (var c = 0, f = k.length; c < f; c += 2) { + var d = h.exec(k[c]); + if (d) { + var j = d[4] + ":" + d[1] + ":" + d[2]; + var b = d[3] || "global code"; + b = b.replace(//, "$1").replace(//, a); + l.push(b + "@" + j + " -- " + k[c + 1].replace(/^\s+/, "")) + } + } + return l + }, + opera10b: function(g) { + var a = "{anonymous}", + h = /^(.*)@(.+):(\d+)$/; + var j = g.stacktrace.split("\n"), + k = []; + for (var c = 0, f = j.length; c < f; c++) { + var d = h.exec(j[c]); + if (d) { + var b = d[1] ? (d[1] + "()") : "global code"; + k.push(b + "@" + d[2] + ":" + d[3]) + } + } + return k + }, + opera10a: function(g) { + var a = "{anonymous}", + h = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i; + var j = g.stacktrace.split("\n"), + k = []; + for (var c = 0, f = j.length; c < f; c += 2) { + var d = h.exec(j[c]); + if (d) { + var b = d[3] || a; + k.push(b + "()@" + d[2] + ":" + d[1] + " -- " + j[c + 1].replace(/^\s+/, "")) + } + } + return k + }, + opera9: function(j) { + var d = "{anonymous}", + h = /Line (\d+).*script (?:in )?(\S+)/i; + var c = j.message.split("\n"), + b = []; + for (var g = 2, a = c.length; g < a; g += 2) { + var f = h.exec(c[g]); + if (f) { + b.push(d + "()@" + f[2] + ":" + f[1] + " -- " + c[g + 1].replace(/^\s+/, "")) + } + } + return b + }, + other: function(g) { + var b = "{anonymous}", + f = /function\s*([\w\-$]+)?\s*\(/i, + a = [], + d, c, e = 10; + while (g && a.length < e) { + d = f.test(g.toString()) ? RegExp.$1 || b : b; + c = Array.prototype.slice.call(g["arguments"] || []); + a[a.length] = d + "(" + this.stringifyArguments(c) + ")"; + g = g.caller + } + return a + }, + stringifyArguments: function(c) { + var b = []; + var e = Array.prototype.slice; + for (var d = 0; d < c.length; ++d) { + var a = c[d]; + if (a === undefined) { + b[d] = "undefined" + } else { + if (a === null) { + b[d] = "null" + } else { + if (a.constructor) { + if (a.constructor === Array) { + if (a.length < 3) { + b[d] = "[" + this.stringifyArguments(a) + "]" + } else { + b[d] = "[" + this.stringifyArguments(e.call(a, 0, 1)) + "..." + this.stringifyArguments(e.call(a, -1)) + "]" + } + } else { + if (a.constructor === Object) { + b[d] = "#object" + } else { + if (a.constructor === Function) { + b[d] = "#function" + } else { + if (a.constructor === String) { + b[d] = '"' + a + '"' + } else { + if (a.constructor === Number) { + b[d] = a + } + } + } + } + } + } + } + } + } + return b.join(",") + }, + sourceCache: {}, + ajax: function(a) { + var b = this.createXMLHTTPObject(); + if (b) { + try { + b.open("GET", a, false); + b.send(null); + return b.responseText + } catch (c) {} + } + return "" + }, + createXMLHTTPObject: function() { + var c, a = [function() { + return new XMLHttpRequest() + }, function() { + return new ActiveXObject("Msxml2.XMLHTTP") + }, function() { + return new ActiveXObject("Msxml3.XMLHTTP") + }, function() { + return new ActiveXObject("Microsoft.XMLHTTP") + }]; + for (var b = 0; b < a.length; b++) { + try { + c = a[b](); + this.createXMLHTTPObject = a[b]; + return c + } catch (d) {} + } + }, + isSameDomain: function(a) { + return a.indexOf(location.hostname) !== -1 + }, + getSource: function(a) { + if (!(a in this.sourceCache)) { + this.sourceCache[a] = this.ajax(a).split("\n") + } + return this.sourceCache[a] + }, + guessAnonymousFunctions: function(k) { + for (var g = 0; g < k.length; ++g) { + var f = /\{anonymous\}\(.*\)@(.*)/, + l = /^(.*?)(?::(\d+))(?::(\d+))?(?: -- .+)?$/, + b = k[g], + c = f.exec(b); + if (c) { + var e = l.exec(c[1]), + d = e[1], + a = e[2], + j = e[3] || 0; + if (d && this.isSameDomain(d) && a) { + var h = this.guessAnonymousFunction(d, a, j); + k[g] = b.replace("{anonymous}", h) + } + } + } + return k + }, + guessAnonymousFunction: function(c, f, a) { + var b; + try { + b = this.findFunctionName(this.getSource(c), f) + } catch (d) { + b = "getSource failed with url: " + c + ", exception: " + d.toString() + } + return b + }, + findFunctionName: function(a, e) { + var g = /function\s+([^(]*?)\s*\(([^)]*)\)/; + var k = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function\b/; + var h = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(?:eval|new Function)\b/; + var b = "", + l, j = Math.min(e, 20), + d, c; + for (var f = 0; f < j; ++f) { + l = a[e - f - 1]; + c = l.indexOf("//"); + if (c >= 0) { + l = l.substr(0, c) + } + if (l) { + b = l + b; + d = k.exec(b); + if (d && d[1]) { + return d[1] + } + d = g.exec(b); + if (d && d[1]) { + return d[1] + } + d = h.exec(b); + if (d && d[1]) { + return d[1] + } + } + } + return "(?)" + } +}; +CanvasRenderingContext2D.prototype.installPattern = function(e) { + if (typeof(this.isPatternInstalled) !== "undefined") { + throw "Must un-install old line pattern before installing a new one." + } + this.isPatternInstalled = true; + var g = [0, 0]; + var b = []; + var f = this.beginPath; + var d = this.lineTo; + var c = this.moveTo; + var a = this.stroke; + this.uninstallPattern = function() { + this.beginPath = f; + this.lineTo = d; + this.moveTo = c; + this.stroke = a; + this.uninstallPattern = undefined; + this.isPatternInstalled = undefined + }; + this.beginPath = function() { + b = []; + f.call(this) + }; + this.moveTo = function(h, i) { + b.push([ + [h, i] + ]); + c.call(this, h, i) + }; + this.lineTo = function(h, j) { + var i = b[b.length - 1]; + i.push([h, j]) + }; + this.stroke = function() { + if (b.length === 0) { + a.call(this); + return + } + for (var p = 0; p < b.length; p++) { + var o = b[p]; + var l = o[0][0], + u = o[0][1]; + for (var n = 1; n < o.length; n++) { + var h = o[n][0], + s = o[n][1]; + this.save(); + var w = (h - l); + var v = (s - u); + var r = Math.sqrt(w * w + v * v); + var k = Math.atan2(v, w); + this.translate(l, u); + c.call(this, 0, 0); + this.rotate(k); + var m = g[0]; + var t = 0; + while (r > t) { + var q = e[m]; + if (g[1]) { + t += g[1] + } else { + t += q + } + if (t > r) { + g = [m, t - r]; + t = r + } else { + g = [(m + 1) % e.length, 0] + } + if (m % 2 === 0) { + d.call(this, t, 0) + } else { + c.call(this, t, 0) + } + m = (m + 1) % e.length + } + this.restore(); + l = h, u = s + } + } + a.call(this); + b = [] + } +}; +CanvasRenderingContext2D.prototype.uninstallPattern = function() { + throw "Must install a line pattern before uninstalling it." +}; +var DygraphOptions = (function() { + var a = function(b) { + this.dygraph_ = b; + this.yAxes_ = []; + this.xAxis_ = {}; + this.series_ = {}; + this.global_ = this.dygraph_.attrs_; + this.user_ = this.dygraph_.user_attrs_ || {}; + this.labels_ = []; + this.highlightSeries_ = this.get("highlightSeriesOpts") || {}; + this.reparseSeries() + }; + a.AXIS_STRING_MAPPINGS_ = { + y: 0, + Y: 0, + y1: 0, + Y1: 0, + y2: 1, + Y2: 1 + }; + a.axisToIndex_ = function(b) { + if (typeof(b) == "string") { + if (a.AXIS_STRING_MAPPINGS_.hasOwnProperty(b)) { + return a.AXIS_STRING_MAPPINGS_[b] + } + throw "Unknown axis : " + b + } + if (typeof(b) == "number") { + if (b === 0 || b === 1) { + return b + } + throw "Dygraphs only supports two y-axes, indexed from 0-1." + } + if (b) { + throw "Unknown axis : " + b + } + return 0 + }; + a.prototype.reparseSeries = function() { + var g = this.get("labels"); + if (!g) { + return + } + this.labels_ = g.slice(1); + this.yAxes_ = [{ + series: [], + options: {} + }]; + this.xAxis_ = { + options: {} + }; + this.series_ = {}; + var h = !this.user_.series; + if (h) { + var c = 0; + for (var j = 0; j < this.labels_.length; j++) { + var i = this.labels_[j]; + var e = this.user_[i] || {}; + var b = 0; + var d = e.axis; + if (typeof(d) == "object") { + b = ++c; + this.yAxes_[b] = { + series: [i], + options: d + } + } + if (!d) { + this.yAxes_[0].series.push(i) + } + this.series_[i] = { + idx: j, + yAxis: b, + options: e + } + } + for (var j = 0; j < this.labels_.length; j++) { + var i = this.labels_[j]; + var e = this.series_[i]["options"]; + var d = e.axis; + if (typeof(d) == "string") { + if (!this.series_.hasOwnProperty(d)) { + Dygraph.error("Series " + i + " wants to share a y-axis with series " + d + ", which does not define its own axis."); + return + } + var b = this.series_[d].yAxis; + this.series_[i].yAxis = b; + this.yAxes_[b].series.push(i) + } + } + } else { + for (var j = 0; j < this.labels_.length; j++) { + var i = this.labels_[j]; + var e = this.user_.series[i] || {}; + var b = a.axisToIndex_(e.axis); + this.series_[i] = { + idx: j, + yAxis: b, + options: e + }; + if (!this.yAxes_[b]) { + this.yAxes_[b] = { + series: [i], + options: {} + } + } else { + this.yAxes_[b].series.push(i) + } + } + } + var f = this.user_.axes || {}; + Dygraph.update(this.yAxes_[0].options, f.y || {}); + if (this.yAxes_.length > 1) { + Dygraph.update(this.yAxes_[1].options, f.y2 || {}) + } + Dygraph.update(this.xAxis_.options, f.x || {}) + }; + a.prototype.get = function(c) { + var b = this.getGlobalUser_(c); + if (b !== null) { + return b + } + return this.getGlobalDefault_(c) + }; + a.prototype.getGlobalUser_ = function(b) { + if (this.user_.hasOwnProperty(b)) { + return this.user_[b] + } + return null + }; + a.prototype.getGlobalDefault_ = function(b) { + if (this.global_.hasOwnProperty(b)) { + return this.global_[b] + } + if (Dygraph.DEFAULT_ATTRS.hasOwnProperty(b)) { + return Dygraph.DEFAULT_ATTRS[b] + } + return null + }; + a.prototype.getForAxis = function(d, e) { + var f; + var i; + if (typeof(e) == "number") { + f = e; + i = f === 0 ? "y" : "y2" + } else { + if (e == "y1") { + e = "y" + } + if (e == "y") { + f = 0 + } else { + if (e == "y2") { + f = 1 + } else { + if (e == "x") { + f = -1 + } else { + throw "Unknown axis " + e + } + } + } + i = e + } + var g = (f == -1) ? this.xAxis_ : this.yAxes_[f]; + if (g) { + var h = g.options; + if (h.hasOwnProperty(d)) { + return h[d] + } + } + var c = this.getGlobalUser_(d); + if (c !== null) { + return c + } + var b = Dygraph.DEFAULT_ATTRS.axes[i]; + if (b.hasOwnProperty(d)) { + return b[d] + } + return this.getGlobalDefault_(d) + }; + a.prototype.getForSeries = function(c, e) { + if (e === this.dygraph_.getHighlightSeries()) { + if (this.highlightSeries_.hasOwnProperty(c)) { + return this.highlightSeries_[c] + } + } + if (!this.series_.hasOwnProperty(e)) { + throw "Unknown series: " + e + } + var d = this.series_[e]; + var b = d.options; + if (b.hasOwnProperty(c)) { + return b[c] + } + return this.getForAxis(c, d.yAxis) + }; + a.prototype.numAxes = function() { + return this.yAxes_.length + }; + a.prototype.axisForSeries = function(b) { + return this.series_[b].yAxis + }; + a.prototype.axisOptions = function(b) { + return this.yAxes_[b].options + }; + a.prototype.seriesForAxis = function(b) { + return this.yAxes_[b].series + }; + a.prototype.seriesNames = function() { + return this.labels_ + }; + return a +})(); +"use strict"; +var DygraphLayout = function(a) { + this.dygraph_ = a; + this.points = []; + this.setNames = []; + this.annotations = []; + this.yAxes_ = null; + this.xTicks_ = null; + this.yTicks_ = null +}; +DygraphLayout.prototype.attr_ = function(a) { + return this.dygraph_.attr_(a) +}; +DygraphLayout.prototype.addDataset = function(a, b) { + this.points.push(b); + this.setNames.push(a) +}; +DygraphLayout.prototype.getPlotArea = function() { + return this.area_ +}; +DygraphLayout.prototype.computePlotArea = function() { + var a = { + x: 0, + y: 0 + }; + a.w = this.dygraph_.width_ - a.x - this.attr_("rightGap"); + a.h = this.dygraph_.height_; + var b = { + chart_div: this.dygraph_.graphDiv, + reserveSpaceLeft: function(c) { + var d = { + x: a.x, + y: a.y, + w: c, + h: a.h + }; + a.x += c; + a.w -= c; + return d + }, + reserveSpaceRight: function(c) { + var d = { + x: a.x + a.w - c, + y: a.y, + w: c, + h: a.h + }; + a.w -= c; + return d + }, + reserveSpaceTop: function(c) { + var d = { + x: a.x, + y: a.y, + w: a.w, + h: c + }; + a.y += c; + a.h -= c; + return d + }, + reserveSpaceBottom: function(c) { + var d = { + x: a.x, + y: a.y + a.h - c, + w: a.w, + h: c + }; + a.h -= c; + return d + }, + chartRect: function() { + return { + x: a.x, + y: a.y, + w: a.w, + h: a.h + } + } + }; + this.dygraph_.cascadeEvents_("layout", b); + this.area_ = a +}; +DygraphLayout.prototype.setAnnotations = function(d) { + this.annotations = []; + var e = this.attr_("xValueParser") || function(a) { + return a + }; + for (var c = 0; c < d.length; c++) { + var b = {}; + if (!d[c].xval && d[c].x === undefined) { + this.dygraph_.error("Annotations must have an 'x' property"); + return + } + if (d[c].icon && !(d[c].hasOwnProperty("width") && d[c].hasOwnProperty("height"))) { + this.dygraph_.error("Must set width and height when setting annotation.icon property"); + return + } + Dygraph.update(b, d[c]); + if (!b.xval) { + b.xval = e(b.x) + } + this.annotations.push(b) + } +}; +DygraphLayout.prototype.setXTicks = function(a) { + this.xTicks_ = a +}; +DygraphLayout.prototype.setYAxes = function(a) { + this.yAxes_ = a +}; +DygraphLayout.prototype.evaluate = function() { + this._evaluateLimits(); + this._evaluateLineCharts(); + this._evaluateLineTicks(); + this._evaluateAnnotations() +}; +DygraphLayout.prototype._evaluateLimits = function() { + var a = this.dygraph_.xAxisRange(); + this.minxval = a[0]; + this.maxxval = a[1]; + var d = a[1] - a[0]; + this.xscale = (d !== 0 ? 1 / d : 1); + for (var b = 0; b < this.yAxes_.length; b++) { + var c = this.yAxes_[b]; + c.minyval = c.computedValueRange[0]; + c.maxyval = c.computedValueRange[1]; + c.yrange = c.maxyval - c.minyval; + c.yscale = (c.yrange !== 0 ? 1 / c.yrange : 1); + if (c.g.attr_("logscale")) { + c.ylogrange = Dygraph.log10(c.maxyval) - Dygraph.log10(c.minyval); + c.ylogscale = (c.ylogrange !== 0 ? 1 / c.ylogrange : 1); + if (!isFinite(c.ylogrange) || isNaN(c.ylogrange)) { + c.g.error("axis " + b + " of graph at " + c.g + " can't be displayed in log scale for range [" + c.minyval + " - " + c.maxyval + "]") + } + } + } +}; +DygraphLayout._calcYNormal = function(b, c, a) { + if (a) { + return 1 - ((Dygraph.log10(c) - Dygraph.log10(b.minyval)) * b.ylogscale) + } else { + return 1 - ((c - b.minyval) * b.yscale) + } +}; +DygraphLayout.prototype._evaluateLineCharts = function() { + var c = this.attr_("connectSeparatedPoints"); + var k = this.attr_("stackedGraph"); + var e = this.attr_("errorBars") || this.attr_("customBars"); + for (var a = 0; a < this.points.length; a++) { + var l = this.points[a]; + var f = this.setNames[a]; + var b = this.dygraph_.axisPropertiesForSeries(f); + var g = this.dygraph_.attributes_.getForSeries("logscale", f); + for (var d = 0; d < l.length; d++) { + var i = l[d]; + i.x = (i.xval - this.minxval) * this.xscale; + var h = i.yval; + if (k) { + i.y_stacked = DygraphLayout._calcYNormal(b, i.yval_stacked, g); + if (h !== null && !isNaN(h)) { + h = i.yval_stacked + } + } + if (h === null) { + h = NaN; + if (!c) { + i.yval = NaN + } + } + i.y = DygraphLayout._calcYNormal(b, h, g); + if (e) { + i.y_top = DygraphLayout._calcYNormal(b, h - i.yval_minus, g); + i.y_bottom = DygraphLayout._calcYNormal(b, h + i.yval_plus, g) + } + } + } +}; +DygraphLayout.parseFloat_ = function(a) { + if (a === null) { + return NaN + } + return a +}; +DygraphLayout.prototype._evaluateLineTicks = function() { + var d, c, b, f; + this.xticks = []; + for (d = 0; d < this.xTicks_.length; d++) { + c = this.xTicks_[d]; + b = c.label; + f = this.xscale * (c.v - this.minxval); + if ((f >= 0) && (f <= 1)) { + this.xticks.push([f, b]) + } + } + this.yticks = []; + for (d = 0; d < this.yAxes_.length; d++) { + var e = this.yAxes_[d]; + for (var a = 0; a < e.ticks.length; a++) { + c = e.ticks[a]; + b = c.label; + f = this.dygraph_.toPercentYCoord(c.v, d); + if ((f >= 0) && (f <= 1)) { + this.yticks.push([d, f, b]) + } + } + } +}; +DygraphLayout.prototype._evaluateAnnotations = function() { + var d; + var g = {}; + for (d = 0; d < this.annotations.length; d++) { + var b = this.annotations[d]; + g[b.xval + "," + b.series] = b + } + this.annotated_points = []; + if (!this.annotations || !this.annotations.length) { + return + } + for (var h = 0; h < this.points.length; h++) { + var e = this.points[h]; + for (d = 0; d < e.length; d++) { + var f = e[d]; + var c = f.xval + "," + f.name; + if (c in g) { + f.annotation = g[c]; + this.annotated_points.push(f) + } + } + } +}; +DygraphLayout.prototype.removeAllDatasets = function() { + delete this.points; + delete this.setNames; + delete this.setPointsLengths; + delete this.setPointsOffsets; + this.points = []; + this.setNames = []; + this.setPointsLengths = []; + this.setPointsOffsets = [] +}; +"use strict"; +var DygraphCanvasRenderer = function(d, c, b, e) { + this.dygraph_ = d; + this.layout = e; + this.element = c; + this.elementContext = b; + this.container = this.element.parentNode; + this.height = this.element.height; + this.width = this.element.width; + if (!this.isIE && !(DygraphCanvasRenderer.isSupported(this.element))) { + throw "Canvas is not supported." + } + this.area = e.getPlotArea(); + this.container.style.position = "relative"; + this.container.style.width = this.width + "px"; + if (this.dygraph_.isUsingExcanvas_) { + this._createIEClipArea() + } else { + if (!Dygraph.isAndroid()) { + var a = this.dygraph_.canvas_ctx_; + a.beginPath(); + a.rect(this.area.x, this.area.y, this.area.w, this.area.h); + a.clip(); + a = this.dygraph_.hidden_ctx_; + a.beginPath(); + a.rect(this.area.x, this.area.y, this.area.w, this.area.h); + a.clip() + } + } +}; +DygraphCanvasRenderer.prototype.attr_ = function(a, b) { + return this.dygraph_.attr_(a, b) +}; +DygraphCanvasRenderer.prototype.clear = function() { + var a; + if (this.isIE) { + try { + if (this.clearDelay) { + this.clearDelay.cancel(); + this.clearDelay = null + } + a = this.elementContext + } catch (b) { + return + } + } + a = this.elementContext; + a.clearRect(0, 0, this.width, this.height) +}; +DygraphCanvasRenderer.isSupported = function(f) { + var b = null; + try { + if (typeof(f) == "undefined" || f === null) { + b = document.createElement("canvas") + } else { + b = f + } + b.getContext("2d") + } catch (c) { + var d = navigator.appVersion.match(/MSIE (\d\.\d)/); + var a = (navigator.userAgent.toLowerCase().indexOf("opera") != -1); + if ((!d) || (d[1] < 6) || (a)) { + return false + } + return true + } + return true +}; +DygraphCanvasRenderer.prototype.render = function() { + this._updatePoints(); + this._renderLineChart() +}; +DygraphCanvasRenderer.prototype._createIEClipArea = function() { + var g = "dygraph-clip-div"; + var f = this.dygraph_.graphDiv; + for (var e = f.childNodes.length - 1; e >= 0; e--) { + if (f.childNodes[e].className == g) { + f.removeChild(f.childNodes[e]) + } + } + var c = document.bgColor; + var d = this.dygraph_.graphDiv; + while (d != document) { + var a = d.currentStyle.backgroundColor; + if (a && a != "transparent") { + c = a; + break + } + d = d.parentNode + } + + function b(j) { + if (j.w === 0 || j.h === 0) { + return + } + var i = document.createElement("div"); + i.className = g; + i.style.backgroundColor = c; + i.style.position = "absolute"; + i.style.left = j.x + "px"; + i.style.top = j.y + "px"; + i.style.width = j.w + "px"; + i.style.height = j.h + "px"; + f.appendChild(i) + } + var h = this.area; + b({ + x: 0, + y: 0, + w: h.x, + h: this.height + }); + b({ + x: h.x, + y: 0, + w: this.width - h.x, + h: h.y + }); + b({ + x: h.x + h.w, + y: 0, + w: this.width - h.x - h.w, + h: this.height + }); + b({ + x: h.x, + y: h.y + h.h, + w: this.width - h.x, + h: this.height - h.h - h.y + }) +}; +DygraphCanvasRenderer._getIteratorPredicate = function(a) { + return a ? DygraphCanvasRenderer._predicateThatSkipsEmptyPoints : null +}; +DygraphCanvasRenderer._predicateThatSkipsEmptyPoints = function(b, a) { + return b[a].yval !== null +}; +DygraphCanvasRenderer._drawStyledLine = function(i, a, m, q, f, n, d) { + var h = i.dygraph; + var c = h.getOption("stepPlot", i.setName); + if (!Dygraph.isArrayLike(q)) { + q = null + } + var l = h.getOption("drawGapEdgePoints", i.setName); + var o = i.points; + var k = Dygraph.createIterator(o, 0, o.length, DygraphCanvasRenderer._getIteratorPredicate(h.getOption("connectSeparatedPoints"))); + var j = q && (q.length >= 2); + var p = i.drawingContext; + p.save(); + if (j) { + p.installPattern(q) + } + var b = DygraphCanvasRenderer._drawSeries(i, k, m, d, f, l, c, a); + DygraphCanvasRenderer._drawPointsOnLine(i, b, n, a, d); + if (j) { + p.uninstallPattern() + } + p.restore() +}; +DygraphCanvasRenderer._drawSeries = function(v, t, m, h, p, s, g, q) { + var b = null; + var w = null; + var k = null; + var j; + var o; + var l = []; + var f = true; + var n = v.drawingContext; + n.beginPath(); + n.strokeStyle = q; + n.lineWidth = m; + var c = t.array_; + var u = t.end_; + var a = t.predicate_; + for (var r = t.start_; r < u; r++) { + o = c[r]; + if (a) { + while (r < u && !a(c, r)) { + r++ + } + if (r == u) { + break + } + o = c[r] + } + if (o.canvasy === null || o.canvasy != o.canvasy) { + if (g && b !== null) { + n.moveTo(b, w); + n.lineTo(o.canvasx, w) + } + b = w = null + } else { + j = false; + if (s || !b) { + t.nextIdx_ = r; + t.next(); + k = t.hasNext ? t.peek.canvasy : null; + var d = k === null || k != k; + j = (!b && d); + if (s) { + if ((!f && !b) || (t.hasNext && d)) { + j = true + } + } + } + if (b !== null) { + if (m) { + if (g) { + n.moveTo(b, w); + n.lineTo(o.canvasx, w) + } + n.lineTo(o.canvasx, o.canvasy) + } + } else { + n.moveTo(o.canvasx, o.canvasy) + } + if (p || j) { + l.push([o.canvasx, o.canvasy, o.idx]) + } + b = o.canvasx; + w = o.canvasy + } + f = false + } + n.stroke(); + return l +}; +DygraphCanvasRenderer._drawPointsOnLine = function(h, i, f, d, g) { + var c = h.drawingContext; + for (var b = 0; b < i.length; b++) { + var a = i[b]; + c.save(); + f(h.dygraph, h.setName, c, a[0], a[1], d, g, a[2]); + c.restore() + } +}; +DygraphCanvasRenderer.prototype._updatePoints = function() { + var e = this.layout.points; + for (var c = e.length; c--;) { + var d = e[c]; + for (var b = d.length; b--;) { + var a = d[b]; + a.canvasx = this.area.w * a.x + this.area.x; + a.canvasy = this.area.h * a.y + this.area.y + } + } +}; +DygraphCanvasRenderer.prototype._renderLineChart = function(g, u) { + var h = u || this.elementContext; + var n; + var a = this.layout.points; + var s = this.layout.setNames; + var b; + this.colors = this.dygraph_.colorsMap_; + var o = this.attr_("plotter"); + var f = o; + if (!Dygraph.isArrayLike(f)) { + f = [f] + } + var c = {}; + for (n = 0; n < s.length; n++) { + b = s[n]; + var t = this.attr_("plotter", b); + if (t == o) { + continue + } + c[b] = t + } + for (n = 0; n < f.length; n++) { + var r = f[n]; + var q = (n == f.length - 1); + for (var l = 0; l < a.length; l++) { + b = s[l]; + if (g && b != g) { + continue + } + var m = a[l]; + var e = r; + if (b in c) { + if (q) { + e = c[b] + } else { + continue + } + } + var k = this.colors[b]; + var d = this.dygraph_.getOption("strokeWidth", b); + h.save(); + h.strokeStyle = k; + h.lineWidth = d; + e({ + points: m, + setName: b, + drawingContext: h, + color: k, + strokeWidth: d, + dygraph: this.dygraph_, + axis: this.dygraph_.axisPropertiesForSeries(b), + plotArea: this.area, + seriesIndex: l, + seriesCount: a.length, + singleSeriesName: g, + allSeriesPoints: a + }); + h.restore() + } + } +}; +DygraphCanvasRenderer._Plotters = { + linePlotter: function(a) { + DygraphCanvasRenderer._linePlotter(a) + }, + fillPlotter: function(a) { + DygraphCanvasRenderer._fillPlotter(a) + }, + errorPlotter: function(a) { + DygraphCanvasRenderer._errorPlotter(a) + } +}; +DygraphCanvasRenderer._linePlotter = function(f) { + var d = f.dygraph; + var h = f.setName; + var i = f.strokeWidth; + var a = d.getOption("strokeBorderWidth", h); + var j = d.getOption("drawPointCallback", h) || Dygraph.Circles.DEFAULT; + var k = d.getOption("strokePattern", h); + var c = d.getOption("drawPoints", h); + var b = d.getOption("pointSize", h); + if (a && i) { + DygraphCanvasRenderer._drawStyledLine(f, d.getOption("strokeBorderColor", h), i + 2 * a, k, c, j, b) + } + DygraphCanvasRenderer._drawStyledLine(f, f.color, i, k, c, j, b) +}; +DygraphCanvasRenderer._errorPlotter = function(s) { + var r = s.dygraph; + var f = s.setName; + var t = r.getOption("errorBars") || r.getOption("customBars"); + if (!t) { + return + } + var k = r.getOption("fillGraph", f); + if (k) { + r.warn("Can't use fillGraph option with error bars") + } + var m = s.drawingContext; + var n = s.color; + var o = r.getOption("fillAlpha", f); + var i = r.getOption("stepPlot", f); + var p = s.points; + var q = Dygraph.createIterator(p, 0, p.length, DygraphCanvasRenderer._getIteratorPredicate(r.getOption("connectSeparatedPoints"))); + var j; + var h = NaN; + var c = NaN; + var d = [-1, -1]; + var a = new RGBColorParser(n); + var u = "rgba(" + a.r + "," + a.g + "," + a.b + "," + o + ")"; + m.fillStyle = u; + m.beginPath(); + var b = function(e) { + return (e === null || e === undefined || isNaN(e)) + }; + while (q.hasNext) { + var l = q.next(); + if ((!i && b(l.y)) || (i && !isNaN(c) && b(c))) { + h = NaN; + continue + } + if (i) { + j = [l.y_bottom, l.y_top]; + c = l.y + } else { + j = [l.y_bottom, l.y_top] + } + j[0] = s.plotArea.h * j[0] + s.plotArea.y; + j[1] = s.plotArea.h * j[1] + s.plotArea.y; + if (!isNaN(h)) { + if (i) { + m.moveTo(h, d[0]); + m.lineTo(l.canvasx, d[0]); + m.lineTo(l.canvasx, d[1]) + } else { + m.moveTo(h, d[0]); + m.lineTo(l.canvasx, j[0]); + m.lineTo(l.canvasx, j[1]) + } + m.lineTo(h, d[1]); + m.closePath() + } + d = j; + h = l.canvasx + } + m.fill() +}; +DygraphCanvasRenderer._fillPlotter = function(F) { + if (F.singleSeriesName) { + return + } + if (F.seriesIndex !== 0) { + return + } + var D = F.dygraph; + var I = D.getLabels().slice(1); + for (var C = I.length; C >= 0; C--) { + if (!D.visibility()[C]) { + I.splice(C, 1) + } + } + var h = (function() { + for (var e = 0; e < I.length; e++) { + if (D.getOption("fillGraph", I[e])) { + return true + } + } + return false + })(); + if (!h) { + return + } + var w = F.drawingContext; + var E = F.plotArea; + var c = F.allSeriesPoints; + var z = c.length; + var y = D.getOption("fillAlpha"); + var k = D.getOption("stackedGraph"); + var q = D.getColors(); + var s = {}; + var a; + var r; + for (var u = z - 1; u >= 0; u--) { + var n = I[u]; + if (!D.getOption("fillGraph", n)) { + continue + } + var p = D.getOption("stepPlot", n); + var x = q[u]; + var f = D.axisPropertiesForSeries(n); + var d = 1 + f.minyval * f.yscale; + if (d < 0) { + d = 0 + } else { + if (d > 1) { + d = 1 + } + } + d = E.h * d + E.y; + var B = c[u]; + var A = Dygraph.createIterator(B, 0, B.length, DygraphCanvasRenderer._getIteratorPredicate(D.getOption("connectSeparatedPoints"))); + var m = NaN; + var l = [-1, -1]; + var t; + var b = new RGBColorParser(x); + var H = "rgba(" + b.r + "," + b.g + "," + b.b + "," + y + ")"; + w.fillStyle = H; + w.beginPath(); + var j, G = true; + while (A.hasNext) { + var v = A.next(); + if (!Dygraph.isOK(v.y)) { + m = NaN; + if (v.y_stacked !== null && !isNaN(v.y_stacked)) { + s[v.canvasx] = E.h * v.y_stacked + E.y + } + continue + } + if (k) { + if (!G && j == v.xval) { + continue + } else { + G = false; + j = v.xval + } + a = s[v.canvasx]; + var o; + if (a === undefined) { + o = d + } else { + if (r) { + o = a[0] + } else { + o = a + } + } + t = [v.canvasy, o]; + if (p) { + if (l[0] === -1) { + s[v.canvasx] = [v.canvasy, d] + } else { + s[v.canvasx] = [v.canvasy, l[0]] + } + } else { + s[v.canvasx] = v.canvasy + } + } else { + t = [v.canvasy, d] + } + if (!isNaN(m)) { + w.moveTo(m, l[0]); + if (p) { + w.lineTo(v.canvasx, l[0]) + } else { + w.lineTo(v.canvasx, t[0]) + } + if (r && a) { + w.lineTo(v.canvasx, a[1]) + } else { + w.lineTo(v.canvasx, t[1]) + } + w.lineTo(m, l[1]); + w.closePath() + } + l = t; + m = v.canvasx + } + r = p; + w.fill() + } +}; +"use strict"; +var Dygraph = function(d, c, b, a) { + this.is_initial_draw_ = true; + this.readyFns_ = []; + if (a !== undefined) { + this.warn("Using deprecated four-argument dygraph constructor"); + this.__old_init__(d, c, b, a) + } else { + this.__init__(d, c, b) + } +}; +Dygraph.NAME = "Dygraph"; +Dygraph.VERSION = "1.0.1"; +Dygraph.__repr__ = function() { + return "[" + this.NAME + " " + this.VERSION + "]" +}; +Dygraph.toString = function() { + return this.__repr__() +}; +Dygraph.DEFAULT_ROLL_PERIOD = 1; +Dygraph.DEFAULT_WIDTH = 480; +Dygraph.DEFAULT_HEIGHT = 320; +Dygraph.ANIMATION_STEPS = 12; +Dygraph.ANIMATION_DURATION = 200; +Dygraph.KMB_LABELS = ["K", "M", "B", "T", "Q"]; +Dygraph.KMG2_BIG_LABELS = ["k", "M", "G", "T", "P", "E", "Z", "Y"]; +Dygraph.KMG2_SMALL_LABELS = ["m", "u", "n", "p", "f", "a", "z", "y"]; +Dygraph.numberValueFormatter = function(s, a, u, l) { + var e = a("sigFigs"); + if (e !== null) { + return Dygraph.floatFormat(s, e) + } + var c = a("digitsAfterDecimal"); + var o = a("maxNumberWidth"); + var b = a("labelsKMB"); + var r = a("labelsKMG2"); + var q; + if (s !== 0 && (Math.abs(s) >= Math.pow(10, o) || Math.abs(s) < Math.pow(10, -c))) { + q = s.toExponential(c) + } else { + q = "" + Dygraph.round_(s, c) + } + if (b || r) { + var f; + var t = []; + var m = []; + if (b) { + f = 1000; + t = Dygraph.KMB_LABELS + } + if (r) { + if (b) { + Dygraph.warn("Setting both labelsKMB and labelsKMG2. Pick one!") + } + f = 1024; + t = Dygraph.KMG2_BIG_LABELS; + m = Dygraph.KMG2_SMALL_LABELS + } + var p = Math.abs(s); + var d = Dygraph.pow(f, t.length); + for (var h = t.length - 1; h >= 0; h--, d /= f) { + if (p >= d) { + q = Dygraph.round_(s / d, c) + t[h]; + break + } + } + if (r) { + var i = String(s.toExponential()).split("e-"); + if (i.length === 2 && i[1] >= 3 && i[1] <= 24) { + if (i[1] % 3 > 0) { + q = Dygraph.round_(i[0] / Dygraph.pow(10, (i[1] % 3)), c) + } else { + q = Number(i[0]).toFixed(2) + } + q += m[Math.floor(i[1] / 3) - 1] + } + } + } + return q +}; +Dygraph.numberAxisLabelFormatter = function(a, d, c, b) { + return Dygraph.numberValueFormatter(a, c, b) +}; +Dygraph.dateString_ = function(e) { + var i = Dygraph.zeropad; + var h = new Date(e); + var f = "" + h.getFullYear(); + var g = i(h.getMonth() + 1); + var a = i(h.getDate()); + var c = ""; + var b = h.getHours() * 3600 + h.getMinutes() * 60 + h.getSeconds(); + if (b) { + c = " " + Dygraph.hmsString_(e) + } + return f + "/" + g + "/" + a + c +}; +Dygraph.dateAxisFormatter = function(b, c) { + if (c >= Dygraph.DECADAL) { + return b.strftime("%Y") + } else { + if (c >= Dygraph.MONTHLY) { + return b.strftime("%b %y") + } else { + var a = b.getHours() * 3600 + b.getMinutes() * 60 + b.getSeconds() + b.getMilliseconds(); + if (a === 0 || c >= Dygraph.DAILY) { + return new Date(b.getTime() + 3600 * 1000).strftime("%d%b") + } else { + return Dygraph.hmsString_(b.getTime()) + } + } + } +}; +Dygraph.Plotters = DygraphCanvasRenderer._Plotters; +Dygraph.DEFAULT_ATTRS = { + highlightCircleSize: 3, + highlightSeriesOpts: null, + highlightSeriesBackgroundAlpha: 0.5, + labelsDivWidth: 250, + labelsDivStyles: {}, + labelsSeparateLines: false, + labelsShowZeroValues: true, + labelsKMB: false, + labelsKMG2: false, + showLabelsOnHighlight: true, + digitsAfterDecimal: 2, + maxNumberWidth: 6, + sigFigs: null, + strokeWidth: 1, + strokeBorderWidth: 0, + strokeBorderColor: "white", + axisTickSize: 3, + axisLabelFontSize: 14, + xAxisLabelWidth: 50, + yAxisLabelWidth: 50, + rightGap: 5, + showRoller: false, + xValueParser: Dygraph.dateParser, + delimiter: ",", + sigma: 2, + errorBars: false, + fractions: false, + wilsonInterval: true, + customBars: false, + fillGraph: false, + fillAlpha: 0.15, + connectSeparatedPoints: false, + stackedGraph: false, + stackedGraphNaNFill: "all", + hideOverlayOnMouseOut: true, + legend: "onmouseover", + stepPlot: false, + avoidMinZero: false, + xRangePad: 0, + yRangePad: null, + drawAxesAtZero: false, + titleHeight: 28, + xLabelHeight: 18, + yLabelWidth: 18, + drawXAxis: true, + drawYAxis: true, + axisLineColor: "black", + axisLineWidth: 0.3, + gridLineWidth: 0.3, + axisLabelColor: "black", + axisLabelFont: "Arial", + axisLabelWidth: 50, + drawYGrid: true, + drawXGrid: true, + gridLineColor: "rgb(128,128,128)", + interactionModel: null, + animatedZooms: false, + showRangeSelector: false, + rangeSelectorHeight: 40, + rangeSelectorPlotStrokeColor: "#808FAB", + rangeSelectorPlotFillColor: "#A7B1C4", + plotter: [Dygraph.Plotters.fillPlotter, Dygraph.Plotters.errorPlotter, Dygraph.Plotters.linePlotter], + plugins: [], + axes: { + x: { + pixelsPerLabel: 60, + axisLabelFormatter: Dygraph.dateAxisFormatter, + valueFormatter: Dygraph.dateString_, + drawGrid: true, + independentTicks: true, + ticker: null + }, + y: { + pixelsPerLabel: 30, + valueFormatter: Dygraph.numberValueFormatter, + axisLabelFormatter: Dygraph.numberAxisLabelFormatter, + drawGrid: true, + independentTicks: true, + ticker: null + }, + y2: { + pixelsPerLabel: 30, + valueFormatter: Dygraph.numberValueFormatter, + axisLabelFormatter: Dygraph.numberAxisLabelFormatter, + drawGrid: false, + independentTicks: false, + ticker: null + } + } +}; +Dygraph.HORIZONTAL = 1; +Dygraph.VERTICAL = 2; +Dygraph.PLUGINS = []; +Dygraph.addedAnnotationCSS = false; +Dygraph.prototype.__old_init__ = function(f, d, e, b) { + if (e !== null) { + var a = ["Date"]; + for (var c = 0; c < e.length; c++) { + a.push(e[c]) + } + Dygraph.update(b, { + labels: a + }) + } + this.__init__(f, d, b) +}; +Dygraph.prototype.__init__ = function(a, c, l) { + if (/MSIE/.test(navigator.userAgent) && !window.opera && typeof(G_vmlCanvasManager) != "undefined" && document.readyState != "complete") { + var o = this; + setTimeout(function() { + o.__init__(a, c, l) + }, 100); + return + } + if (l === null || l === undefined) { + l = {} + } + l = Dygraph.mapLegacyOptions_(l); + if (typeof(a) == "string") { + a = document.getElementById(a) + } + if (!a) { + Dygraph.error("Constructing dygraph with a non-existent div!"); + return + } + this.isUsingExcanvas_ = typeof(G_vmlCanvasManager) != "undefined"; + this.maindiv_ = a; + this.file_ = c; + this.rollPeriod_ = l.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD; + this.previousVerticalX_ = -1; + this.fractions_ = l.fractions || false; + this.dateWindow_ = l.dateWindow || null; + this.annotations_ = []; + this.zoomed_x_ = false; + this.zoomed_y_ = false; + a.innerHTML = ""; + if (a.style.width === "" && l.width) { + a.style.width = l.width + "px" + } + if (a.style.height === "" && l.height) { + a.style.height = l.height + "px" + } + if (a.style.height === "" && a.clientHeight === 0) { + a.style.height = Dygraph.DEFAULT_HEIGHT + "px"; + if (a.style.width === "") { + a.style.width = Dygraph.DEFAULT_WIDTH + "px" + } + } + this.width_ = a.clientWidth || l.width || 0; + this.height_ = a.clientHeight || l.height || 0; + if (l.stackedGraph) { + l.fillGraph = true + } + this.user_attrs_ = {}; + Dygraph.update(this.user_attrs_, l); + this.attrs_ = {}; + Dygraph.updateDeep(this.attrs_, Dygraph.DEFAULT_ATTRS); + this.boundaryIds_ = []; + this.setIndexByName_ = {}; + this.datasetIndex_ = []; + this.registeredEvents_ = []; + this.eventListeners_ = {}; + this.attributes_ = new DygraphOptions(this); + this.createInterface_(); + this.plugins_ = []; + var d = Dygraph.PLUGINS.concat(this.getOption("plugins")); + for (var g = 0; g < d.length; g++) { + var k = d[g]; + var f = new k(); + var j = { + plugin: f, + events: {}, + options: {}, + pluginOptions: {} + }; + var b = f.activate(this); + for (var h in b) { + j.events[h] = b[h] + } + this.plugins_.push(j) + } + for (var g = 0; g < this.plugins_.length; g++) { + var n = this.plugins_[g]; + for (var h in n.events) { + if (!n.events.hasOwnProperty(h)) { + continue + } + var m = n.events[h]; + var e = [n.plugin, m]; + if (!(h in this.eventListeners_)) { + this.eventListeners_[h] = [e] + } else { + this.eventListeners_[h].push(e) + } + } + } + this.createDragInterface_(); + this.start_() +}; +Dygraph.prototype.cascadeEvents_ = function(c, b) { + if (!(c in this.eventListeners_)) { + return true + } + var g = { + dygraph: this, + cancelable: false, + defaultPrevented: false, + preventDefault: function() { + if (!g.cancelable) { + throw "Cannot call preventDefault on non-cancelable event." + } + g.defaultPrevented = true + }, + propagationStopped: false, + stopPropagation: function() { + g.propagationStopped = true + } + }; + Dygraph.update(g, b); + var a = this.eventListeners_[c]; + if (a) { + for (var d = a.length - 1; d >= 0; d--) { + var f = a[d][0]; + var h = a[d][1]; + h.call(f, g); + if (g.propagationStopped) { + break + } + } + } + return g.defaultPrevented +}; +Dygraph.prototype.isZoomed = function(a) { + if (a === null || a === undefined) { + return this.zoomed_x_ || this.zoomed_y_ + } + if (a === "x") { + return this.zoomed_x_ + } + if (a === "y") { + return this.zoomed_y_ + } + throw "axis parameter is [" + a + "] must be null, 'x' or 'y'." +}; +Dygraph.prototype.toString = function() { + var a = this.maindiv_; + var b = (a && a.id) ? a.id : a; + return "[Dygraph " + b + "]" +}; +Dygraph.prototype.attr_ = function(b, a) { + return a ? this.attributes_.getForSeries(b, a) : this.attributes_.get(b) +}; +Dygraph.prototype.getOption = function(a, b) { + return this.attr_(a, b) +}; +Dygraph.prototype.getOptionForAxis = function(a, b) { + return this.attributes_.getForAxis(a, b) +}; +Dygraph.prototype.optionsViewForAxis_ = function(b) { + var a = this; + return function(c) { + var d = a.user_attrs_.axes; + if (d && d[b] && d[b].hasOwnProperty(c)) { + return d[b][c] + } + if (typeof(a.user_attrs_[c]) != "undefined") { + return a.user_attrs_[c] + } + d = a.attrs_.axes; + if (d && d[b] && d[b].hasOwnProperty(c)) { + return d[b][c] + } + if (b == "y" && a.axes_[0].hasOwnProperty(c)) { + return a.axes_[0][c] + } else { + if (b == "y2" && a.axes_[1].hasOwnProperty(c)) { + return a.axes_[1][c] + } + } + return a.attr_(c) + } +}; +Dygraph.prototype.rollPeriod = function() { + return this.rollPeriod_ +}; +Dygraph.prototype.xAxisRange = function() { + return this.dateWindow_ ? this.dateWindow_ : this.xAxisExtremes() +}; +Dygraph.prototype.xAxisExtremes = function() { + var d = this.attr_("xRangePad") / this.plotter_.area.w; + if (this.numRows() === 0) { + return [0 - d, 1 + d] + } + var c = this.rawData_[0][0]; + var b = this.rawData_[this.rawData_.length - 1][0]; + if (d) { + var a = b - c; + c -= a * d; + b += a * d + } + return [c, b] +}; +Dygraph.prototype.yAxisRange = function(a) { + if (typeof(a) == "undefined") { + a = 0 + } + if (a < 0 || a >= this.axes_.length) { + return null + } + var b = this.axes_[a]; + return [b.computedValueRange[0], b.computedValueRange[1]] +}; +Dygraph.prototype.yAxisRanges = function() { + var a = []; + for (var b = 0; b < this.axes_.length; b++) { + a.push(this.yAxisRange(b)) + } + return a +}; +Dygraph.prototype.toDomCoords = function(a, c, b) { + return [this.toDomXCoord(a), this.toDomYCoord(c, b)] +}; +Dygraph.prototype.toDomXCoord = function(b) { + if (b === null) { + return null + } + var c = this.plotter_.area; + var a = this.xAxisRange(); + return c.x + (b - a[0]) / (a[1] - a[0]) * c.w +}; +Dygraph.prototype.toDomYCoord = function(d, a) { + var c = this.toPercentYCoord(d, a); + if (c === null) { + return null + } + var b = this.plotter_.area; + return b.y + c * b.h +}; +Dygraph.prototype.toDataCoords = function(a, c, b) { + return [this.toDataXCoord(a), this.toDataYCoord(c, b)] +}; +Dygraph.prototype.toDataXCoord = function(b) { + if (b === null) { + return null + } + var c = this.plotter_.area; + var a = this.xAxisRange(); + return a[0] + (b - c.x) / c.w * (a[1] - a[0]) +}; +Dygraph.prototype.toDataYCoord = function(h, b) { + if (h === null) { + return null + } + var c = this.plotter_.area; + var g = this.yAxisRange(b); + if (typeof(b) == "undefined") { + b = 0 + } + if (!this.attributes_.getForAxis("logscale", b)) { + return g[0] + (c.y + c.h - h) / c.h * (g[1] - g[0]) + } else { + var f = (h - c.y) / c.h; + var a = Dygraph.log10(g[1]); + var e = a - (f * (a - Dygraph.log10(g[0]))); + var d = Math.pow(Dygraph.LOG_SCALE, e); + return d + } +}; +Dygraph.prototype.toPercentYCoord = function(f, c) { + if (f === null) { + return null + } + if (typeof(c) == "undefined") { + c = 0 + } + var e = this.yAxisRange(c); + var d; + var b = this.attributes_.getForAxis("logscale", c); + if (!b) { + d = (e[1] - f) / (e[1] - e[0]) + } else { + var a = Dygraph.log10(e[1]); + d = (a - Dygraph.log10(f)) / (a - Dygraph.log10(e[0])) + } + return d +}; +Dygraph.prototype.toPercentXCoord = function(b) { + if (b === null) { + return null + } + var a = this.xAxisRange(); + return (b - a[0]) / (a[1] - a[0]) +}; +Dygraph.prototype.numColumns = function() { + if (!this.rawData_) { + return 0 + } + return this.rawData_[0] ? this.rawData_[0].length : this.attr_("labels").length +}; +Dygraph.prototype.numRows = function() { + if (!this.rawData_) { + return 0 + } + return this.rawData_.length +}; +Dygraph.prototype.getValue = function(b, a) { + if (b < 0 || b > this.rawData_.length) { + return null + } + if (a < 0 || a > this.rawData_[b].length) { + return null + } + return this.rawData_[b][a] +}; +Dygraph.prototype.createInterface_ = function() { + var a = this.maindiv_; + this.graphDiv = document.createElement("div"); + this.graphDiv.style.textAlign = "left"; + a.appendChild(this.graphDiv); + this.canvas_ = Dygraph.createCanvas(); + this.canvas_.style.position = "absolute"; + this.hidden_ = this.createPlotKitCanvas_(this.canvas_); + this.resizeElements_(); + this.canvas_ctx_ = Dygraph.getContext(this.canvas_); + this.hidden_ctx_ = Dygraph.getContext(this.hidden_); + this.graphDiv.appendChild(this.hidden_); + this.graphDiv.appendChild(this.canvas_); + this.mouseEventElement_ = this.createMouseEventElement_(); + this.layout_ = new DygraphLayout(this); + var b = this; + this.mouseMoveHandler_ = function(c) { + b.mouseMove_(c) + }; + this.mouseOutHandler_ = function(f) { + var d = f.target || f.fromElement; + var c = f.relatedTarget || f.toElement; + if (Dygraph.isNodeContainedBy(d, b.graphDiv) && !Dygraph.isNodeContainedBy(c, b.graphDiv)) { + b.mouseOut_(f) + } + }; + this.addAndTrackEvent(window, "mouseout", this.mouseOutHandler_); + this.addAndTrackEvent(this.mouseEventElement_, "mousemove", this.mouseMoveHandler_); + if (!this.resizeHandler_) { + this.resizeHandler_ = function(c) { + b.resize() + }; + this.addAndTrackEvent(window, "resize", this.resizeHandler_) + } +}; +Dygraph.prototype.resizeElements_ = function() { + this.graphDiv.style.width = this.width_ + "px"; + this.graphDiv.style.height = this.height_ + "px"; + this.canvas_.width = this.width_; + this.canvas_.height = this.height_; + this.canvas_.style.width = this.width_ + "px"; + this.canvas_.style.height = this.height_ + "px"; + this.hidden_.width = this.width_; + this.hidden_.height = this.height_; + this.hidden_.style.width = this.width_ + "px"; + this.hidden_.style.height = this.height_ + "px" +}; +Dygraph.prototype.destroy = function() { + this.canvas_ctx_.restore(); + this.hidden_ctx_.restore(); + var a = function(c) { + while (c.hasChildNodes()) { + a(c.firstChild); + c.removeChild(c.firstChild) + } + }; + this.removeTrackedEvents_(); + Dygraph.removeEvent(window, "mouseout", this.mouseOutHandler_); + Dygraph.removeEvent(this.mouseEventElement_, "mousemove", this.mouseMoveHandler_); + Dygraph.removeEvent(window, "resize", this.resizeHandler_); + this.resizeHandler_ = null; + a(this.maindiv_); + var b = function(c) { + for (var d in c) { + if (typeof(c[d]) === "object") { + c[d] = null + } + } + }; + b(this.layout_); + b(this.plotter_); + b(this) +}; +Dygraph.prototype.createPlotKitCanvas_ = function(a) { + var b = Dygraph.createCanvas(); + b.style.position = "absolute"; + b.style.top = a.style.top; + b.style.left = a.style.left; + b.width = this.width_; + b.height = this.height_; + b.style.width = this.width_ + "px"; + b.style.height = this.height_ + "px"; + return b +}; +Dygraph.prototype.createMouseEventElement_ = function() { + if (this.isUsingExcanvas_) { + var a = document.createElement("div"); + a.style.position = "absolute"; + a.style.backgroundColor = "white"; + a.style.filter = "alpha(opacity=0)"; + a.style.width = this.width_ + "px"; + a.style.height = this.height_ + "px"; + this.graphDiv.appendChild(a); + return a + } else { + return this.canvas_ + } +}; +Dygraph.prototype.setColors_ = function() { + var g = this.getLabels(); + var e = g.length - 1; + this.colors_ = []; + this.colorsMap_ = {}; + var a = this.attr_("colors"); + var d; + if (!a) { + var c = this.attr_("colorSaturation") || 1; + var b = this.attr_("colorValue") || 0.5; + var k = Math.ceil(e / 2); + for (d = 1; d <= e; d++) { + if (!this.visibility()[d - 1]) { + continue + } + var h = d % 2 ? Math.ceil(d / 2) : (k + d / 2); + var f = (1 * h / (1 + e)); + var j = Dygraph.hsvToRGB(f, c, b); + this.colors_.push(j); + this.colorsMap_[g[d]] = j + } + } else { + for (d = 0; d < e; d++) { + if (!this.visibility()[d]) { + continue + } + var j = a[d % a.length]; + this.colors_.push(j); + this.colorsMap_[g[1 + d]] = j + } + } +}; +Dygraph.prototype.getColors = function() { + return this.colors_ +}; +Dygraph.prototype.getPropertiesForSeries = function(c) { + var a = -1; + var d = this.getLabels(); + for (var b = 1; b < d.length; b++) { + if (d[b] == c) { + a = b; + break + } + } + if (a == -1) { + return null + } + return { + name: c, + column: a, + visible: this.visibility()[a - 1], + color: this.colorsMap_[c], + axis: 1 + this.attributes_.axisForSeries(c) + } +}; +Dygraph.prototype.createRollInterface_ = function() { + if (!this.roller_) { + this.roller_ = document.createElement("input"); + this.roller_.type = "text"; + this.roller_.style.display = "none"; + this.graphDiv.appendChild(this.roller_) + } + var e = this.attr_("showRoller") ? "block" : "none"; + var d = this.plotter_.area; + var b = { + position: "absolute", + zIndex: 10, + top: (d.y + d.h - 25) + "px", + left: (d.x + 1) + "px", + display: e + }; + this.roller_.size = "2"; + this.roller_.value = this.rollPeriod_; + for (var a in b) { + if (b.hasOwnProperty(a)) { + this.roller_.style[a] = b[a] + } + } + var c = this; + this.roller_.onchange = function() { + c.adjustRoll(c.roller_.value) + } +}; +Dygraph.prototype.dragGetX_ = function(b, a) { + return Dygraph.pageX(b) - a.px +}; +Dygraph.prototype.dragGetY_ = function(b, a) { + return Dygraph.pageY(b) - a.py +}; +Dygraph.prototype.createDragInterface_ = function() { + var c = { + isZooming: false, + isPanning: false, + is2DPan: false, + dragStartX: null, + dragStartY: null, + dragEndX: null, + dragEndY: null, + dragDirection: null, + prevEndX: null, + prevEndY: null, + prevDragDirection: null, + cancelNextDblclick: false, + initialLeftmostDate: null, + xUnitsPerPixel: null, + dateRange: null, + px: 0, + py: 0, + boundedDates: null, + boundedValues: null, + tarp: new Dygraph.IFrameTarp(), + initializeMouseDown: function(j, i, h) { + if (j.preventDefault) { + j.preventDefault() + } else { + j.returnValue = false; + j.cancelBubble = true + } + h.px = Dygraph.findPosX(i.canvas_); + h.py = Dygraph.findPosY(i.canvas_); + h.dragStartX = i.dragGetX_(j, h); + h.dragStartY = i.dragGetY_(j, h); + h.cancelNextDblclick = false; + h.tarp.cover() + } + }; + var f = this.attr_("interactionModel"); + var b = this; + var e = function(g) { + return function(h) { + g(h, b, c) + } + }; + for (var a in f) { + if (!f.hasOwnProperty(a)) { + continue + } + this.addAndTrackEvent(this.mouseEventElement_, a, e(f[a])) + } + var d = function(h) { + if (c.isZooming || c.isPanning) { + c.isZooming = false; + c.dragStartX = null; + c.dragStartY = null + } + if (c.isPanning) { + c.isPanning = false; + c.draggingDate = null; + c.dateRange = null; + for (var g = 0; g < b.axes_.length; g++) { + delete b.axes_[g].draggingValue; + delete b.axes_[g].dragValueRange + } + } + c.tarp.uncover() + }; + this.addAndTrackEvent(document, "mouseup", d) +}; +Dygraph.prototype.drawZoomRect_ = function(e, c, i, b, g, a, f, d) { + var h = this.canvas_ctx_; + if (a == Dygraph.HORIZONTAL) { + h.clearRect(Math.min(c, f), this.layout_.getPlotArea().y, Math.abs(c - f), this.layout_.getPlotArea().h) + } else { + if (a == Dygraph.VERTICAL) { + h.clearRect(this.layout_.getPlotArea().x, Math.min(b, d), this.layout_.getPlotArea().w, Math.abs(b - d)) + } + } + if (e == Dygraph.HORIZONTAL) { + if (i && c) { + h.fillStyle = "rgba(128,128,128,0.33)"; + h.fillRect(Math.min(c, i), this.layout_.getPlotArea().y, Math.abs(i - c), this.layout_.getPlotArea().h) + } + } else { + if (e == Dygraph.VERTICAL) { + if (g && b) { + h.fillStyle = "rgba(128,128,128,0.33)"; + h.fillRect(this.layout_.getPlotArea().x, Math.min(b, g), this.layout_.getPlotArea().w, Math.abs(g - b)) + } + } + } + if (this.isUsingExcanvas_) { + this.currentZoomRectArgs_ = [e, c, i, b, g, 0, 0, 0] + } +}; +Dygraph.prototype.clearZoomRect_ = function() { + this.currentZoomRectArgs_ = null; + this.canvas_ctx_.clearRect(0, 0, this.canvas_.width, this.canvas_.height) +}; +Dygraph.prototype.doZoomX_ = function(c, a) { + this.currentZoomRectArgs_ = null; + var b = this.toDataXCoord(c); + var d = this.toDataXCoord(a); + this.doZoomXDates_(b, d) +}; +Dygraph.zoomAnimationFunction = function(c, b) { + var a = 1.5; + return (1 - Math.pow(a, -c)) / (1 - Math.pow(a, -b)) +}; +Dygraph.prototype.doZoomXDates_ = function(c, e) { + var a = this.xAxisRange(); + var d = [c, e]; + this.zoomed_x_ = true; + var b = this; + this.doAnimatedZoom(a, d, null, null, function() { + if (b.attr_("zoomCallback")) { + b.attr_("zoomCallback")(c, e, b.yAxisRanges()) + } + }) +}; +Dygraph.prototype.doZoomY_ = function(h, f) { + this.currentZoomRectArgs_ = null; + var c = this.yAxisRanges(); + var b = []; + for (var e = 0; e < this.axes_.length; e++) { + var d = this.toDataYCoord(h, e); + var a = this.toDataYCoord(f, e); + b.push([a, d]) + } + this.zoomed_y_ = true; + var g = this; + this.doAnimatedZoom(null, null, c, b, function() { + if (g.attr_("zoomCallback")) { + var i = g.xAxisRange(); + g.attr_("zoomCallback")(i[0], i[1], g.yAxisRanges()) + } + }) +}; +Dygraph.prototype.resetZoom = function() { + var c = false, + d = false, + a = false; + if (this.dateWindow_ !== null) { + c = true; + d = true + } + for (var g = 0; g < this.axes_.length; g++) { + if (typeof(this.axes_[g].valueWindow) !== "undefined" && this.axes_[g].valueWindow !== null) { + c = true; + a = true + } + } + this.clearSelection(); + if (c) { + this.zoomed_x_ = false; + this.zoomed_y_ = false; + var f = this.rawData_[0][0]; + var b = this.rawData_[this.rawData_.length - 1][0]; + if (!this.attr_("animatedZooms")) { + this.dateWindow_ = null; + for (g = 0; g < this.axes_.length; g++) { + if (this.axes_[g].valueWindow !== null) { + delete this.axes_[g].valueWindow + } + } + this.drawGraph_(); + if (this.attr_("zoomCallback")) { + this.attr_("zoomCallback")(f, b, this.yAxisRanges()) + } + return + } + var l = null, + m = null, + k = null, + h = null; + if (d) { + l = this.xAxisRange(); + m = [f, b] + } + if (a) { + k = this.yAxisRanges(); + var n = this.gatherDatasets_(this.rolledSeries_, null); + var o = n.extremes; + this.computeYAxisRanges_(o); + h = []; + for (g = 0; g < this.axes_.length; g++) { + var e = this.axes_[g]; + h.push((e.valueRange !== null && e.valueRange !== undefined) ? e.valueRange : e.extremeRange) + } + } + var j = this; + this.doAnimatedZoom(l, m, k, h, function() { + j.dateWindow_ = null; + for (var p = 0; p < j.axes_.length; p++) { + if (j.axes_[p].valueWindow !== null) { + delete j.axes_[p].valueWindow + } + } + if (j.attr_("zoomCallback")) { + j.attr_("zoomCallback")(f, b, j.yAxisRanges()) + } + }) + } +}; +Dygraph.prototype.doAnimatedZoom = function(a, e, b, c, m) { + var i = this.attr_("animatedZooms") ? Dygraph.ANIMATION_STEPS : 1; + var l = []; + var k = []; + var f, d; + if (a !== null && e !== null) { + for (f = 1; f <= i; f++) { + d = Dygraph.zoomAnimationFunction(f, i); + l[f - 1] = [a[0] * (1 - d) + d * e[0], a[1] * (1 - d) + d * e[1]] + } + } + if (b !== null && c !== null) { + for (f = 1; f <= i; f++) { + d = Dygraph.zoomAnimationFunction(f, i); + var n = []; + for (var g = 0; g < this.axes_.length; g++) { + n.push([b[g][0] * (1 - d) + d * c[g][0], b[g][1] * (1 - d) + d * c[g][1]]) + } + k[f - 1] = n + } + } + var h = this; + Dygraph.repeatAndCleanup(function(p) { + if (k.length) { + for (var o = 0; o < h.axes_.length; o++) { + var j = k[p][o]; + h.axes_[o].valueWindow = [j[0], j[1]] + } + } + if (l.length) { + h.dateWindow_ = l[p] + } + h.drawGraph_() + }, i, Dygraph.ANIMATION_DURATION / i, m) +}; +Dygraph.prototype.getArea = function() { + return this.plotter_.area +}; +Dygraph.prototype.eventToDomCoords = function(c) { + if (c.offsetX && c.offsetY) { + return [c.offsetX, c.offsetY] + } else { + var b = Dygraph.pageX(c) - Dygraph.findPosX(this.mouseEventElement_); + var a = Dygraph.pageY(c) - Dygraph.findPosY(this.mouseEventElement_); + return [b, a] + } +}; +Dygraph.prototype.findClosestRow = function(a) { + var g = Infinity; + var h = -1; + var e = this.layout_.points; + for (var c = 0; c < e.length; c++) { + var l = e[c]; + var d = l.length; + for (var b = 0; b < d; b++) { + var k = l[b]; + if (!Dygraph.isValidPoint(k, true)) { + continue + } + var f = Math.abs(k.canvasx - a); + if (f < g) { + g = f; + h = k.idx + } + } + } + return h +}; +Dygraph.prototype.findClosestPoint = function(f, e) { + var k = Infinity; + var h, o, n, l, d, c, j; + for (var b = this.layout_.points.length - 1; b >= 0; --b) { + var m = this.layout_.points[b]; + for (var g = 0; g < m.length; ++g) { + l = m[g]; + if (!Dygraph.isValidPoint(l)) { + continue + } + o = l.canvasx - f; + n = l.canvasy - e; + h = o * o + n * n; + if (h < k) { + k = h; + d = l; + c = b; + j = l.idx + } + } + } + var a = this.layout_.setNames[c]; + return { + row: j, + seriesName: a, + point: d + } +}; +Dygraph.prototype.findStackedPoint = function(i, h) { + var p = this.findClosestRow(i); + var f, c; + for (var d = 0; d < this.layout_.points.length; ++d) { + var g = this.getLeftBoundary_(d); + var e = p - g; + var l = this.layout_.points[d]; + if (e >= l.length) { + continue + } + var m = l[e]; + if (!Dygraph.isValidPoint(m)) { + continue + } + var j = m.canvasy; + if (i > m.canvasx && e + 1 < l.length) { + var k = l[e + 1]; + if (Dygraph.isValidPoint(k)) { + var o = k.canvasx - m.canvasx; + if (o > 0) { + var a = (i - m.canvasx) / o; + j += a * (k.canvasy - m.canvasy) + } + } + } else { + if (i < m.canvasx && e > 0) { + var n = l[e - 1]; + if (Dygraph.isValidPoint(n)) { + var o = m.canvasx - n.canvasx; + if (o > 0) { + var a = (m.canvasx - i) / o; + j += a * (n.canvasy - m.canvasy) + } + } + } + } + if (d === 0 || j < h) { + f = m; + c = d + } + } + var b = this.layout_.setNames[c]; + return { + row: p, + seriesName: b, + point: f + } +}; +Dygraph.prototype.mouseMove_ = function(b) { + var h = this.layout_.points; + if (h === undefined || h === null) { + return + } + var e = this.eventToDomCoords(b); + var a = e[0]; + var j = e[1]; + var f = this.attr_("highlightSeriesOpts"); + var d = false; + if (f && !this.isSeriesLocked()) { + var c; + if (this.attr_("stackedGraph")) { + c = this.findStackedPoint(a, j) + } else { + c = this.findClosestPoint(a, j) + } + d = this.setSelection(c.row, c.seriesName) + } else { + var g = this.findClosestRow(a); + d = this.setSelection(g) + } + var i = this.attr_("highlightCallback"); + if (i && d) { + i(b, this.lastx_, this.selPoints_, this.lastRow_, this.highlightSet_) + } +}; +Dygraph.prototype.getLeftBoundary_ = function(b) { + if (this.boundaryIds_[b]) { + return this.boundaryIds_[b][0] + } else { + for (var a = 0; a < this.boundaryIds_.length; a++) { + if (this.boundaryIds_[a] !== undefined) { + return this.boundaryIds_[a][0] + } + } + return 0 + } +}; +Dygraph.prototype.animateSelection_ = function(f) { + var e = 10; + var c = 30; + if (this.fadeLevel === undefined) { + this.fadeLevel = 0 + } + if (this.animateId === undefined) { + this.animateId = 0 + } + var g = this.fadeLevel; + var b = f < 0 ? g : e - g; + if (b <= 0) { + if (this.fadeLevel) { + this.updateSelection_(1) + } + return + } + var a = ++this.animateId; + var d = this; + Dygraph.repeatAndCleanup(function(h) { + if (d.animateId != a) { + return + } + d.fadeLevel += f; + if (d.fadeLevel === 0) { + d.clearSelection() + } else { + d.updateSelection_(d.fadeLevel / e) + } + }, b, c, function() {}) +}; +Dygraph.prototype.updateSelection_ = function(d) { + this.cascadeEvents_("select", { + selectedX: this.lastx_, + selectedPoints: this.selPoints_ + }); + var h; + var n = this.canvas_ctx_; + if (this.attr_("highlightSeriesOpts")) { + n.clearRect(0, 0, this.width_, this.height_); + var f = 1 - this.attr_("highlightSeriesBackgroundAlpha"); + if (f) { + var g = true; + if (g) { + if (d === undefined) { + this.animateSelection_(1); + return + } + f *= d + } + n.fillStyle = "rgba(255,255,255," + f + ")"; + n.fillRect(0, 0, this.width_, this.height_) + } + this.plotter_._renderLineChart(this.highlightSet_, n) + } else { + if (this.previousVerticalX_ >= 0) { + var j = 0; + var k = this.attr_("labels"); + for (h = 1; h < k.length; h++) { + var c = this.attr_("highlightCircleSize", k[h]); + if (c > j) { + j = c + } + } + var l = this.previousVerticalX_; + n.clearRect(l - j - 1, 0, 2 * j + 2, this.height_) + } + } + if (this.isUsingExcanvas_ && this.currentZoomRectArgs_) { + Dygraph.prototype.drawZoomRect_.apply(this, this.currentZoomRectArgs_) + } + if (this.selPoints_.length > 0) { + var b = this.selPoints_[0].canvasx; + n.save(); + for (h = 0; h < this.selPoints_.length; h++) { + var o = this.selPoints_[h]; + if (!Dygraph.isOK(o.canvasy)) { + continue + } + var a = this.attr_("highlightCircleSize", o.name); + var m = this.attr_("drawHighlightPointCallback", o.name); + var e = this.plotter_.colors[o.name]; + if (!m) { + m = Dygraph.Circles.DEFAULT + } + n.lineWidth = this.attr_("strokeWidth", o.name); + n.strokeStyle = e; + n.fillStyle = e; + m(this.g, o.name, n, b, o.canvasy, e, a, o.idx) + } + n.restore(); + this.previousVerticalX_ = b + } +}; +Dygraph.prototype.setSelection = function(f, h, g) { + this.selPoints_ = []; + var e = false; + if (f !== false && f >= 0) { + if (f != this.lastRow_) { + e = true + } + this.lastRow_ = f; + for (var d = 0; d < this.layout_.points.length; ++d) { + var b = this.layout_.points[d]; + var c = f - this.getLeftBoundary_(d); + if (c < b.length) { + var a = b[c]; + if (a.yval !== null) { + this.selPoints_.push(a) + } + } + } + } else { + if (this.lastRow_ >= 0) { + e = true + } + this.lastRow_ = -1 + } + if (this.selPoints_.length) { + this.lastx_ = this.selPoints_[0].xval + } else { + this.lastx_ = -1 + } + if (h !== undefined) { + if (this.highlightSet_ !== h) { + e = true + } + this.highlightSet_ = h + } + if (g !== undefined) { + this.lockedSet_ = g + } + if (e) { + this.updateSelection_(undefined) + } + return e +}; +Dygraph.prototype.mouseOut_ = function(a) { + if (this.attr_("unhighlightCallback")) { + this.attr_("unhighlightCallback")(a) + } + if (this.attr_("hideOverlayOnMouseOut") && !this.lockedSet_) { + this.clearSelection() + } +}; +Dygraph.prototype.clearSelection = function() { + this.cascadeEvents_("deselect", {}); + this.lockedSet_ = false; + if (this.fadeLevel) { + this.animateSelection_(-1); + return + } + this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_); + this.fadeLevel = 0; + this.selPoints_ = []; + this.lastx_ = -1; + this.lastRow_ = -1; + this.highlightSet_ = null +}; +Dygraph.prototype.getSelection = function() { + if (!this.selPoints_ || this.selPoints_.length < 1) { + return -1 + } + for (var c = 0; c < this.layout_.points.length; c++) { + var a = this.layout_.points[c]; + for (var b = 0; b < a.length; b++) { + if (a[b].x == this.selPoints_[0].x) { + return a[b].idx + } + } + } + return -1 +}; +Dygraph.prototype.getHighlightSeries = function() { + return this.highlightSet_ +}; +Dygraph.prototype.isSeriesLocked = function() { + return this.lockedSet_ +}; +Dygraph.prototype.loadedEvent_ = function(a) { + this.rawData_ = this.parseCSV_(a); + this.predraw_() +}; +Dygraph.prototype.addXTicks_ = function() { + var a; + if (this.dateWindow_) { + a = [this.dateWindow_[0], this.dateWindow_[1]] + } else { + a = this.xAxisExtremes() + } + var c = this.optionsViewForAxis_("x"); + var b = c("ticker")(a[0], a[1], this.width_, c, this); + this.layout_.setXTicks(b) +}; +Dygraph.prototype.extremeValues_ = function(d) { + var h = null, + f = null, + c, g; + var b = this.attr_("errorBars") || this.attr_("customBars"); + if (b) { + for (c = 0; c < d.length; c++) { + g = d[c][1][0]; + if (g === null || isNaN(g)) { + continue + } + var a = g - d[c][1][1]; + var e = g + d[c][1][2]; + if (a > g) { + a = g + } + if (e < g) { + e = g + } + if (f === null || e > f) { + f = e + } + if (h === null || a < h) { + h = a + } + } + } else { + for (c = 0; c < d.length; c++) { + g = d[c][1]; + if (g === null || isNaN(g)) { + continue + } + if (f === null || g > f) { + f = g + } + if (h === null || g < h) { + h = g + } + } + } + return [h, f] +}; +Dygraph.prototype.predraw_ = function() { + var e = new Date(); + this.layout_.computePlotArea(); + this.computeYAxes_(); + if (this.plotter_) { + this.cascadeEvents_("clearChart"); + this.plotter_.clear() + } + if (!this.is_initial_draw_) { + this.canvas_ctx_.restore(); + this.hidden_ctx_.restore() + } + this.canvas_ctx_.save(); + this.hidden_ctx_.save(); + this.plotter_ = new DygraphCanvasRenderer(this, this.hidden_, this.hidden_ctx_, this.layout_); + this.createRollInterface_(); + this.cascadeEvents_("predraw"); + this.rolledSeries_ = [null]; + for (var c = 1; c < this.numColumns(); c++) { + var d = this.attr_("logscale"); + var b = this.extractSeries_(this.rawData_, c, d); + b = this.rollingAverage(b, this.rollPeriod_); + this.rolledSeries_.push(b) + } + this.drawGraph_(); + var a = new Date(); + this.drawingTimeMs_ = (a - e) +}; +Dygraph.PointType = undefined; +Dygraph.seriesToPoints_ = function(b, j, d, f) { + var h = []; + for (var c = 0; c < b.length; ++c) { + var k = b[c]; + var a = j ? k[1][0] : k[1]; + var e = a === null ? null : DygraphLayout.parseFloat_(a); + var g = { + x: NaN, + y: NaN, + xval: DygraphLayout.parseFloat_(k[0]), + yval: e, + name: d, + idx: c + f + }; + if (j) { + g.y_top = NaN; + g.y_bottom = NaN; + g.yval_minus = DygraphLayout.parseFloat_(k[1][1]); + g.yval_plus = DygraphLayout.parseFloat_(k[1][2]) + } + h.push(g) + } + return h +}; +Dygraph.stackPoints_ = function(m, l, o, f) { + var h = null; + var d = null; + var g = null; + var c = -1; + var k = function(i) { + if (c >= i) { + return + } + for (var p = i; p < m.length; ++p) { + g = null; + if (!isNaN(m[p].yval) && m[p].yval !== null) { + c = p; + g = m[p]; + break + } + } + }; + for (var b = 0; b < m.length; ++b) { + var j = m[b]; + var n = j.xval; + if (l[n] === undefined) { + l[n] = 0 + } + var e = j.yval; + if (isNaN(e) || e === null) { + k(b); + if (d && g && f != "none") { + e = d.yval + (g.yval - d.yval) * ((n - d.xval) / (g.xval - d.xval)) + } else { + if (d && f == "all") { + e = d.yval + } else { + if (g && f == "all") { + e = g.yval + } else { + e = 0 + } + } + } + } else { + d = j + } + var a = l[n]; + if (h != n) { + a += e; + l[n] = a + } + h = n; + j.yval_stacked = a; + if (a > o[1]) { + o[1] = a + } + if (a < o[0]) { + o[0] = a + } + } +}; +Dygraph.prototype.gatherDatasets_ = function(w, f) { + var s = []; + var t = []; + var q = []; + var a = {}; + var u, r; + var x = this.attr_("errorBars"); + var h = this.attr_("customBars"); + var p = x || h; + var b = function(i) { + if (!p) { + return i[1] === null + } else { + return h ? i[1][1] === null : x ? i[1][0] === null : false + } + }; + var n = w.length - 1; + var l; + for (u = n; u >= 1; u--) { + if (!this.visibility()[u - 1]) { + continue + } + if (f) { + l = w[u]; + var z = f[0]; + var j = f[1]; + var g = null, + y = null; + for (r = 0; r < l.length; r++) { + if (l[r][0] >= z && g === null) { + g = r + } + if (l[r][0] <= j) { + y = r + } + } + if (g === null) { + g = 0 + } + var e = g; + var d = true; + while (d && e > 0) { + e--; + d = b(l[e]) + } + if (y === null) { + y = l.length - 1 + } + var c = y; + d = true; + while (d && c < l.length - 1) { + c++; + d = b(l[c]) + } + if (e !== g) { + g = e + } + if (c !== y) { + y = c + } + s[u - 1] = [g, y]; + l = l.slice(g, y + 1) + } else { + l = w[u]; + s[u - 1] = [0, l.length - 1] + } + var v = this.attr_("labels")[u]; + var o = this.extremeValues_(l); + var m = Dygraph.seriesToPoints_(l, p, v, s[u - 1][0]); + if (this.attr_("stackedGraph")) { + Dygraph.stackPoints_(m, q, o, this.attr_("stackedGraphNaNFill")) + } + a[v] = o; + t[u] = m + } + return { + points: t, + extremes: a, + boundaryIds: s + } +}; +Dygraph.prototype.drawGraph_ = function() { + var a = new Date(); + var d = this.is_initial_draw_; + this.is_initial_draw_ = false; + this.layout_.removeAllDatasets(); + this.setColors_(); + this.attrs_.pointSize = 0.5 * this.attr_("highlightCircleSize"); + var j = this.gatherDatasets_(this.rolledSeries_, this.dateWindow_); + var h = j.points; + var k = j.extremes; + this.boundaryIds_ = j.boundaryIds; + this.setIndexByName_ = {}; + var g = this.attr_("labels"); + if (g.length > 0) { + this.setIndexByName_[g[0]] = 0 + } + var e = 0; + for (var f = 1; f < h.length; f++) { + this.setIndexByName_[g[f]] = f; + if (!this.visibility()[f - 1]) { + continue + } + this.layout_.addDataset(g[f], h[f]); + this.datasetIndex_[f] = e++ + } + this.computeYAxisRanges_(k); + this.layout_.setYAxes(this.axes_); + this.addXTicks_(); + var b = this.zoomed_x_; + this.zoomed_x_ = b; + this.layout_.evaluate(); + this.renderGraph_(d); + if (this.attr_("timingName")) { + var c = new Date(); + Dygraph.info(this.attr_("timingName") + " - drawGraph: " + (c - a) + "ms") + } +}; +Dygraph.prototype.renderGraph_ = function(b) { + this.cascadeEvents_("clearChart"); + this.plotter_.clear(); + if (this.attr_("underlayCallback")) { + this.attr_("underlayCallback")(this.hidden_ctx_, this.layout_.getPlotArea(), this, this) + } + var c = { + canvas: this.hidden_, + drawingContext: this.hidden_ctx_ + }; + this.cascadeEvents_("willDrawChart", c); + this.plotter_.render(); + this.cascadeEvents_("didDrawChart", c); + this.lastRow_ = -1; + this.canvas_.getContext("2d").clearRect(0, 0, this.canvas_.width, this.canvas_.height); + if (this.attr_("drawCallback") !== null) { + this.attr_("drawCallback")(this, b) + } + if (b) { + this.readyFired_ = true; + while (this.readyFns_.length > 0) { + var a = this.readyFns_.pop(); + a(this) + } + } +}; +Dygraph.prototype.computeYAxes_ = function() { + var b, d, c, f, a; + if (this.axes_ !== undefined && this.user_attrs_.hasOwnProperty("valueRange") === false) { + b = []; + for (c = 0; c < this.axes_.length; c++) { + b.push(this.axes_[c].valueWindow) + } + } + this.axes_ = []; + for (d = 0; d < this.attributes_.numAxes(); d++) { + f = { + g: this + }; + Dygraph.update(f, this.attributes_.axisOptions(d)); + this.axes_[d] = f + } + a = this.attr_("valueRange"); + if (a) { + this.axes_[0].valueRange = a + } + if (b !== undefined) { + var e = Math.min(b.length, this.axes_.length); + for (c = 0; c < e; c++) { + this.axes_[c].valueWindow = b[c] + } + } + for (d = 0; d < this.axes_.length; d++) { + if (d === 0) { + f = this.optionsViewForAxis_("y" + (d ? "2" : "")); + a = f("valueRange"); + if (a) { + this.axes_[d].valueRange = a + } + } else { + var g = this.user_attrs_.axes; + if (g && g.y2) { + a = g.y2.valueRange; + if (a) { + this.axes_[d].valueRange = a + } + } + } + } +}; +Dygraph.prototype.numAxes = function() { + return this.attributes_.numAxes() +}; +Dygraph.prototype.axisPropertiesForSeries = function(a) { + return this.axes_[this.attributes_.axisForSeries(a)] +}; +Dygraph.prototype.computeYAxisRanges_ = function(a) { + var g = function(i) { + return isNaN(parseFloat(i)) + }; + var q = this.attributes_.numAxes(); + var b, x, o, B; + var p; + for (var y = 0; y < q; y++) { + var c = this.axes_[y]; + var C = this.attributes_.getForAxis("logscale", y); + var G = this.attributes_.getForAxis("includeZero", y); + var l = this.attributes_.getForAxis("independentTicks", y); + o = this.attributes_.seriesForAxis(y); + b = true; + B = 0.1; + if (this.attr_("yRangePad") !== null) { + b = false; + B = this.attr_("yRangePad") / this.plotter_.area.h + } + if (o.length === 0) { + c.extremeRange = [0, 1] + } else { + var D = Infinity; + var A = -Infinity; + var t, s; + for (var w = 0; w < o.length; w++) { + if (!a.hasOwnProperty(o[w])) { + continue + } + t = a[o[w]][0]; + if (t !== null) { + D = Math.min(t, D) + } + s = a[o[w]][1]; + if (s !== null) { + A = Math.max(s, A) + } + } + if (G && !C) { + if (D > 0) { + D = 0 + } + if (A < 0) { + A = 0 + } + } + if (D == Infinity) { + D = 0 + } + if (A == -Infinity) { + A = 1 + } + x = A - D; + if (x === 0) { + if (A !== 0) { + x = Math.abs(A) + } else { + A = 1; + x = 1 + } + } + var h, H; + if (C) { + if (b) { + h = A + B * x; + H = D + } else { + var E = Math.exp(Math.log(x) * B); + h = A * E; + H = D / E + } + } else { + h = A + B * x; + H = D - B * x; + if (b && !this.attr_("avoidMinZero")) { + if (H < 0 && D >= 0) { + H = 0 + } + if (h > 0 && A <= 0) { + h = 0 + } + } + } + c.extremeRange = [H, h] + } + if (c.valueWindow) { + c.computedValueRange = [c.valueWindow[0], c.valueWindow[1]] + } else { + if (c.valueRange) { + var e = g(c.valueRange[0]) ? c.extremeRange[0] : c.valueRange[0]; + var d = g(c.valueRange[1]) ? c.extremeRange[1] : c.valueRange[1]; + if (!b) { + if (c.logscale) { + var E = Math.exp(Math.log(x) * B); + e *= E; + d /= E + } else { + x = d - e; + e -= x * B; + d += x * B + } + } + c.computedValueRange = [e, d] + } else { + c.computedValueRange = c.extremeRange + } + } + if (l) { + c.independentTicks = l; + var r = this.optionsViewForAxis_("y" + (y ? "2" : "")); + var F = r("ticker"); + c.ticks = F(c.computedValueRange[0], c.computedValueRange[1], this.height_, r, this); + if (!p) { + p = c + } + } + } + if (p === undefined) { + throw ('Configuration Error: At least one axis has to have the "independentTicks" option activated.') + } + for (var y = 0; y < q; y++) { + var c = this.axes_[y]; + if (!c.independentTicks) { + var r = this.optionsViewForAxis_("y" + (y ? "2" : "")); + var F = r("ticker"); + var m = p.ticks; + var n = p.computedValueRange[1] - p.computedValueRange[0]; + var I = c.computedValueRange[1] - c.computedValueRange[0]; + var f = []; + for (var v = 0; v < m.length; v++) { + var u = (m[v].v - p.computedValueRange[0]) / n; + var z = c.computedValueRange[0] + u * I; + f.push(z) + } + c.ticks = F(c.computedValueRange[0], c.computedValueRange[1], this.height_, r, this, f) + } + } +}; +Dygraph.prototype.extractSeries_ = function(a, f, c) { + var g = []; + var h = this.attr_("errorBars"); + var e = this.attr_("customBars"); + for (var d = 0; d < a.length; d++) { + var l = a[d][0]; + var m = a[d][f]; + if (c) { + if (h || e) { + for (var b = 0; b < m.length; b++) { + if (m[b] <= 0) { + m = null; + break + } + } + } else { + if (m <= 0) { + m = null + } + } + } + if (m !== null) { + g.push([l, m]) + } else { + g.push([l, h ? [null, null] : e ? [null, null, null] : m]) + } + } + return g +}; +Dygraph.prototype.rollingAverage = function(l, d) { + d = Math.min(d, l.length); + var b = []; + var t = this.attr_("sigma"); + var G, o, z, x, m, c, F, A; + if (this.fractions_) { + var k = 0; + var h = 0; + var e = 100; + for (z = 0; z < l.length; z++) { + k += l[z][1][0]; + h += l[z][1][1]; + if (z - d >= 0) { + k -= l[z - d][1][0]; + h -= l[z - d][1][1] + } + var C = l[z][0]; + var w = h ? k / h : 0; + if (this.attr_("errorBars")) { + if (this.attr_("wilsonInterval")) { + if (h) { + var s = w < 0 ? 0 : w, + u = h; + var B = t * Math.sqrt(s * (1 - s) / u + t * t / (4 * u * u)); + var a = 1 + t * t / h; + G = (s + t * t / (2 * h) - B) / a; + o = (s + t * t / (2 * h) + B) / a; + b[z] = [C, [s * e, (s - G) * e, (o - s) * e]] + } else { + b[z] = [C, [0, 0, 0]] + } + } else { + A = h ? t * Math.sqrt(w * (1 - w) / h) : 1; + b[z] = [C, [e * w, e * A, e * A]] + } + } else { + b[z] = [C, e * w] + } + } + } else { + if (this.attr_("customBars")) { + G = 0; + var D = 0; + o = 0; + var g = 0; + for (z = 0; z < l.length; z++) { + var E = l[z][1]; + m = E[1]; + b[z] = [l[z][0], + [m, m - E[0], E[2] - m] + ]; + if (m !== null && !isNaN(m)) { + G += E[0]; + D += m; + o += E[2]; + g += 1 + } + if (z - d >= 0) { + var r = l[z - d]; + if (r[1][1] !== null && !isNaN(r[1][1])) { + G -= r[1][0]; + D -= r[1][1]; + o -= r[1][2]; + g -= 1 + } + } + if (g) { + b[z] = [l[z][0], + [1 * D / g, 1 * (D - G) / g, 1 * (o - D) / g] + ] + } else { + b[z] = [l[z][0], + [null, null, null] + ] + } + } + } else { + if (!this.attr_("errorBars")) { + if (d == 1) { + return l + } + for (z = 0; z < l.length; z++) { + c = 0; + F = 0; + for (x = Math.max(0, z - d + 1); x < z + 1; x++) { + m = l[x][1]; + if (m === null || isNaN(m)) { + continue + } + F++; + c += l[x][1] + } + if (F) { + b[z] = [l[z][0], c / F] + } else { + b[z] = [l[z][0], null] + } + } + } else { + for (z = 0; z < l.length; z++) { + c = 0; + var f = 0; + F = 0; + for (x = Math.max(0, z - d + 1); x < z + 1; x++) { + m = l[x][1][0]; + if (m === null || isNaN(m)) { + continue + } + F++; + c += l[x][1][0]; + f += Math.pow(l[x][1][1], 2) + } + if (F) { + A = Math.sqrt(f) / F; + b[z] = [l[z][0], + [c / F, t * A, t * A] + ] + } else { + var q = (d == 1) ? l[z][1][0] : null; + b[z] = [l[z][0], + [q, q, q] + ] + } + } + } + } + } + return b +}; +Dygraph.prototype.detectTypeFromString_ = function(b) { + var a = false; + var c = b.indexOf("-"); + if ((c > 0 && (b[c - 1] != "e" && b[c - 1] != "E")) || b.indexOf("/") >= 0 || isNaN(parseFloat(b))) { + a = true + } else { + if (b.length == 8 && b > "19700101" && b < "20371231") { + a = true + } + } + this.setXAxisOptions_(a) +}; +Dygraph.prototype.setXAxisOptions_ = function(a) { + if (a) { + this.attrs_.xValueParser = Dygraph.dateParser; + this.attrs_.axes.x.valueFormatter = Dygraph.dateString_; + this.attrs_.axes.x.ticker = Dygraph.dateTicker; + this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter + } else { + this.attrs_.xValueParser = function(b) { + return parseFloat(b) + }; + this.attrs_.axes.x.valueFormatter = function(b) { + return b + }; + this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks; + this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter + } +}; +Dygraph.prototype.parseFloat_ = function(a, c, b) { + var e = parseFloat(a); + if (!isNaN(e)) { + return e + } + if (/^ *$/.test(a)) { + return null + } + if (/^ *nan *$/i.test(a)) { + return NaN + } + var d = "Unable to parse '" + a + "' as a number"; + if (b !== null && c !== null) { + d += " on line " + (1 + c) + " ('" + b + "') of CSV." + } + this.error(d); + return null +}; +Dygraph.prototype.parseCSV_ = function(t) { + var r = []; + var s = Dygraph.detectLineDelimiter(t); + var a = t.split(s || "\n"); + var g, k; + var p = this.attr_("delimiter"); + if (a[0].indexOf(p) == -1 && a[0].indexOf("\t") >= 0) { + p = "\t" + } + var b = 0; + if (!("labels" in this.user_attrs_)) { + b = 1; + this.attrs_.labels = a[0].split(p); + this.attributes_.reparseSeries() + } + var o = 0; + var m; + var q = false; + var c = this.attr_("labels").length; + var f = false; + for (var l = b; l < a.length; l++) { + var e = a[l]; + o = l; + if (e.length === 0) { + continue + } + if (e[0] == "#") { + continue + } + var d = e.split(p); + if (d.length < 2) { + continue + } + var h = []; + if (!q) { + this.detectTypeFromString_(d[0]); + m = this.attr_("xValueParser"); + q = true + } + h[0] = m(d[0], this); + if (this.fractions_) { + for (k = 1; k < d.length; k++) { + g = d[k].split("/"); + if (g.length != 2) { + this.error('Expected fractional "num/den" values in CSV data but found a value \'' + d[k] + "' on line " + (1 + l) + " ('" + e + "') which is not of this form."); + h[k] = [0, 0] + } else { + h[k] = [this.parseFloat_(g[0], l, e), this.parseFloat_(g[1], l, e)] + } + } + } else { + if (this.attr_("errorBars")) { + if (d.length % 2 != 1) { + this.error("Expected alternating (value, stdev.) pairs in CSV data but line " + (1 + l) + " has an odd number of values (" + (d.length - 1) + "): '" + e + "'") + } + for (k = 1; k < d.length; k += 2) { + h[(k + 1) / 2] = [this.parseFloat_(d[k], l, e), this.parseFloat_(d[k + 1], l, e)] + } + } else { + if (this.attr_("customBars")) { + for (k = 1; k < d.length; k++) { + var u = d[k]; + if (/^ *$/.test(u)) { + h[k] = [null, null, null] + } else { + g = u.split(";"); + if (g.length == 3) { + h[k] = [this.parseFloat_(g[0], l, e), this.parseFloat_(g[1], l, e), this.parseFloat_(g[2], l, e)] + } else { + this.warn('When using customBars, values must be either blank or "low;center;high" tuples (got "' + u + '" on line ' + (1 + l)) + } + } + } + } else { + for (k = 1; k < d.length; k++) { + h[k] = this.parseFloat_(d[k], l, e) + } + } + } + } + if (r.length > 0 && h[0] < r[r.length - 1][0]) { + f = true + } + if (h.length != c) { + this.error("Number of columns in line " + l + " (" + h.length + ") does not agree with number of labels (" + c + ") " + e) + } + if (l === 0 && this.attr_("labels")) { + var n = true; + for (k = 0; n && k < h.length; k++) { + if (h[k]) { + n = false + } + } + if (n) { + this.warn("The dygraphs 'labels' option is set, but the first row of CSV data ('" + e + "') appears to also contain labels. Will drop the CSV labels and use the option labels."); + continue + } + } + r.push(h) + } + if (f) { + this.warn("CSV is out of order; order it correctly to speed loading."); + r.sort(function(j, i) { + return j[0] - i[0] + }) + } + return r +}; +Dygraph.prototype.parseArray_ = function(c) { + if (c.length === 0) { + this.error("Can't plot empty data set"); + return null + } + if (c[0].length === 0) { + this.error("Data set cannot contain an empty row"); + return null + } + var a; + if (this.attr_("labels") === null) { + this.warn("Using default labels. Set labels explicitly via 'labels' in the options parameter"); + this.attrs_.labels = ["X"]; + for (a = 1; a < c[0].length; a++) { + this.attrs_.labels.push("Y" + a) + } + this.attributes_.reparseSeries() + } else { + var b = this.attr_("labels"); + if (b.length != c[0].length) { + this.error("Mismatch between number of labels (" + b + ") and number of columns in array (" + c[0].length + ")"); + return null + } + } + if (Dygraph.isDateLike(c[0][0])) { + this.attrs_.axes.x.valueFormatter = Dygraph.dateString_; + this.attrs_.axes.x.ticker = Dygraph.dateTicker; + this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter; + var d = Dygraph.clone(c); + for (a = 0; a < c.length; a++) { + if (d[a].length === 0) { + this.error("Row " + (1 + a) + " of data is empty"); + return null + } + if (d[a][0] === null || typeof(d[a][0].getTime) != "function" || isNaN(d[a][0].getTime())) { + this.error("x value in row " + (1 + a) + " is not a Date"); + return null + } + d[a][0] = d[a][0].getTime() + } + return d + } else { + this.attrs_.axes.x.valueFormatter = function(e) { + return e + }; + this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks; + this.attrs_.axes.x.axisLabelFormatter = Dygraph.numberAxisLabelFormatter; + return c + } +}; +Dygraph.prototype.parseDataTable_ = function(w) { + var d = function(i) { + var j = String.fromCharCode(65 + i % 26); + i = Math.floor(i / 26); + while (i > 0) { + j = String.fromCharCode(65 + (i - 1) % 26) + j.toLowerCase(); + i = Math.floor((i - 1) / 26) + } + return j + }; + var h = w.getNumberOfColumns(); + var g = w.getNumberOfRows(); + var f = w.getColumnType(0); + if (f == "date" || f == "datetime") { + this.attrs_.xValueParser = Dygraph.dateParser; + this.attrs_.axes.x.valueFormatter = Dygraph.dateString_; + this.attrs_.axes.x.ticker = Dygraph.dateTicker; + this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter + } else { + if (f == "number") { + this.attrs_.xValueParser = function(i) { + return parseFloat(i) + }; + this.attrs_.axes.x.valueFormatter = function(i) { + return i + }; + this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks; + this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter + } else { + this.error("only 'date', 'datetime' and 'number' types are supported for column 1 of DataTable input (Got '" + f + "')"); + return null + } + } + var m = []; + var t = {}; + var s = false; + var q, o; + for (q = 1; q < h; q++) { + var b = w.getColumnType(q); + if (b == "number") { + m.push(q) + } else { + if (b == "string" && this.attr_("displayAnnotations")) { + var r = m[m.length - 1]; + if (!t.hasOwnProperty(r)) { + t[r] = [q] + } else { + t[r].push(q) + } + s = true + } else { + this.error("Only 'number' is supported as a dependent type with Gviz. 'string' is only supported if displayAnnotations is true") + } + } + } + var u = [w.getColumnLabel(0)]; + for (q = 0; q < m.length; q++) { + u.push(w.getColumnLabel(m[q])); + if (this.attr_("errorBars")) { + q += 1 + } + } + this.attrs_.labels = u; + h = u.length; + var v = []; + var l = false; + var a = []; + for (q = 0; q < g; q++) { + var e = []; + if (typeof(w.getValue(q, 0)) === "undefined" || w.getValue(q, 0) === null) { + this.warn("Ignoring row " + q + " of DataTable because of undefined or null first column."); + continue + } + if (f == "date" || f == "datetime") { + e.push(w.getValue(q, 0).getTime()) + } else { + e.push(w.getValue(q, 0)) + } + if (!this.attr_("errorBars")) { + for (o = 0; o < m.length; o++) { + var c = m[o]; + e.push(w.getValue(q, c)); + if (s && t.hasOwnProperty(c) && w.getValue(q, t[c][0]) !== null) { + var p = {}; + p.series = w.getColumnLabel(c); + p.xval = e[0]; + p.shortText = d(a.length); + p.text = ""; + for (var n = 0; n < t[c].length; n++) { + if (n) { + p.text += "\n" + } + p.text += w.getValue(q, t[c][n]) + } + a.push(p) + } + } + for (o = 0; o < e.length; o++) { + if (!isFinite(e[o])) { + e[o] = null + } + } + } else { + for (o = 0; o < h - 1; o++) { + e.push([w.getValue(q, 1 + 2 * o), w.getValue(q, 2 + 2 * o)]) + } + } + if (v.length > 0 && e[0] < v[v.length - 1][0]) { + l = true + } + v.push(e) + } + if (l) { + this.warn("DataTable is out of order; order it correctly to speed loading."); + v.sort(function(j, i) { + return j[0] - i[0] + }) + } + this.rawData_ = v; + if (a.length > 0) { + this.setAnnotations(a, true) + } + this.attributes_.reparseSeries() +}; +Dygraph.prototype.start_ = function() { + var d = this.file_; + if (typeof d == "function") { + d = d() + } + if (Dygraph.isArrayLike(d)) { + this.rawData_ = this.parseArray_(d); + this.predraw_() + } else { + if (typeof d == "object" && typeof d.getColumnRange == "function") { + this.parseDataTable_(d); + this.predraw_() + } else { + if (typeof d == "string") { + var c = Dygraph.detectLineDelimiter(d); + if (c) { + this.loadedEvent_(d) + } else { + var b; + if (window.XMLHttpRequest) { + b = new XMLHttpRequest() + } else { + b = new ActiveXObject("Microsoft.XMLHTTP") + } + var a = this; + b.onreadystatechange = function() { + if (b.readyState == 4) { + if (b.status === 200 || b.status === 0) { + a.loadedEvent_(b.responseText) + } + } + }; + b.open("GET", d, true); + b.send(null) + } + } else { + this.error("Unknown data format: " + (typeof d)) + } + } + } +}; +Dygraph.prototype.updateOptions = function(e, b) { + if (typeof(b) == "undefined") { + b = false + } + var d = e.file; + var c = Dygraph.mapLegacyOptions_(e); + if ("rollPeriod" in c) { + this.rollPeriod_ = c.rollPeriod + } + if ("dateWindow" in c) { + this.dateWindow_ = c.dateWindow; + if (!("isZoomedIgnoreProgrammaticZoom" in c)) { + this.zoomed_x_ = (c.dateWindow !== null) + } + } + if ("valueRange" in c && !("isZoomedIgnoreProgrammaticZoom" in c)) { + this.zoomed_y_ = (c.valueRange !== null) + } + var a = Dygraph.isPixelChangingOptionList(this.attr_("labels"), c); + Dygraph.updateDeep(this.user_attrs_, c); + this.attributes_.reparseSeries(); + if (d) { + this.file_ = d; + if (!b) { + this.start_() + } + } else { + if (!b) { + if (a) { + this.predraw_() + } else { + this.renderGraph_(false) + } + } + } +}; +Dygraph.mapLegacyOptions_ = function(c) { + var a = {}; + for (var b in c) { + if (b == "file") { + continue + } + if (c.hasOwnProperty(b)) { + a[b] = c[b] + } + } + var e = function(g, f, h) { + if (!a.axes) { + a.axes = {} + } + if (!a.axes[g]) { + a.axes[g] = {} + } + a.axes[g][f] = h + }; + var d = function(f, g, h) { + if (typeof(c[f]) != "undefined") { + Dygraph.warn("Option " + f + " is deprecated. Use the " + h + " option for the " + g + " axis instead. (e.g. { axes : { " + g + " : { " + h + " : ... } } } (see http://dygraphs.com/per-axis.html for more information."); + e(g, h, c[f]); + delete a[f] + } + }; + d("xValueFormatter", "x", "valueFormatter"); + d("pixelsPerXLabel", "x", "pixelsPerLabel"); + d("xAxisLabelFormatter", "x", "axisLabelFormatter"); + d("xTicker", "x", "ticker"); + d("yValueFormatter", "y", "valueFormatter"); + d("pixelsPerYLabel", "y", "pixelsPerLabel"); + d("yAxisLabelFormatter", "y", "axisLabelFormatter"); + d("yTicker", "y", "ticker"); + return a +}; +Dygraph.prototype.resize = function(d, b) { + if (this.resize_lock) { + return + } + this.resize_lock = true; + if ((d === null) != (b === null)) { + this.warn("Dygraph.resize() should be called with zero parameters or two non-NULL parameters. Pretending it was zero."); + d = b = null + } + var a = this.width_; + var c = this.height_; + if (d) { + this.maindiv_.style.width = d + "px"; + this.maindiv_.style.height = b + "px"; + this.width_ = d; + this.height_ = b + } else { + this.width_ = this.maindiv_.clientWidth; + this.height_ = this.maindiv_.clientHeight + } + if (a != this.width_ || c != this.height_) { + this.resizeElements_(); + this.predraw_() + } + this.resize_lock = false +}; +Dygraph.prototype.adjustRoll = function(a) { + this.rollPeriod_ = a; + this.predraw_() +}; +Dygraph.prototype.visibility = function() { + if (!this.attr_("visibility")) { + this.attrs_.visibility = [] + } + while (this.attr_("visibility").length < this.numColumns() - 1) { + this.attrs_.visibility.push(true) + } + return this.attr_("visibility") +}; +Dygraph.prototype.setVisibility = function(b, c) { + var a = this.visibility(); + if (b < 0 || b >= a.length) { + this.warn("invalid series number in setVisibility: " + b) + } else { + a[b] = c; + this.predraw_() + } +}; +Dygraph.prototype.size = function() { + return { + width: this.width_, + height: this.height_ + } +}; +Dygraph.prototype.setAnnotations = function(b, a) { + Dygraph.addAnnotationRule(); + this.annotations_ = b; + if (!this.layout_) { + this.warn("Tried to setAnnotations before dygraph was ready. Try setting them in a ready() block. See dygraphs.com/tests/annotation.html"); + return + } + this.layout_.setAnnotations(this.annotations_); + if (!a) { + this.predraw_() + } +}; +Dygraph.prototype.annotations = function() { + return this.annotations_ +}; +Dygraph.prototype.getLabels = function() { + var a = this.attr_("labels"); + return a ? a.slice() : null +}; +Dygraph.prototype.indexFromSetName = function(a) { + return this.setIndexByName_[a] +}; +Dygraph.prototype.ready = function(a) { + if (this.is_initial_draw_) { + this.readyFns_.push(a) + } else { + a(this) + } +}; +Dygraph.addAnnotationRule = function() { + if (Dygraph.addedAnnotationCSS) { + return + } + var f = "border: 1px solid black; background-color: white; text-align: center;"; + var e = document.createElement("style"); + e.type = "text/css"; + document.getElementsByTagName("head")[0].appendChild(e); + for (var b = 0; b < document.styleSheets.length; b++) { + if (document.styleSheets[b].disabled) { + continue + } + var d = document.styleSheets[b]; + try { + if (d.insertRule) { + var a = d.cssRules ? d.cssRules.length : 0; + d.insertRule(".dygraphDefaultAnnotation { " + f + " }", a) + } else { + if (d.addRule) { + d.addRule(".dygraphDefaultAnnotation", f) + } + } + Dygraph.addedAnnotationCSS = true; + return + } catch (c) {} + } + this.warn("Unable to add default annotation CSS rule; display may be off.") +}; +var DateGraph = Dygraph; +"use strict"; +Dygraph.LOG_SCALE = 10; +Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE); +Dygraph.log10 = function(a) { + return Math.log(a) / Dygraph.LN_TEN +}; +Dygraph.DEBUG = 1; +Dygraph.INFO = 2; +Dygraph.WARNING = 3; +Dygraph.ERROR = 3; +Dygraph.LOG_STACK_TRACES = false; +Dygraph.DOTTED_LINE = [2, 2]; +Dygraph.DASHED_LINE = [7, 3]; +Dygraph.DOT_DASH_LINE = [7, 2, 2, 2]; +Dygraph.log = function(c, g) { + var b; + if (typeof(printStackTrace) != "undefined") { + try { + b = printStackTrace({ + guess: false + }); + while (b[0].indexOf("stacktrace") != -1) { + b.splice(0, 1) + } + b.splice(0, 2); + for (var d = 0; d < b.length; d++) { + b[d] = b[d].replace(/\([^)]*\/(.*)\)/, "@$1").replace(/\@.*\/([^\/]*)/, "@$1").replace("[object Object].", "") + } + var j = b.splice(0, 1)[0]; + g += " (" + j.replace(/^.*@ ?/, "") + ")" + } catch (h) {} + } + if (typeof(window.console) != "undefined") { + var a = window.console; + var f = function(e, k, i) { + if (k && typeof(k) == "function") { + k.call(e, i) + } else { + e.log(i) + } + }; + switch (c) { + case Dygraph.DEBUG: + f(a, a.debug, "dygraphs: " + g); + break; + case Dygraph.INFO: + f(a, a.info, "dygraphs: " + g); + break; + case Dygraph.WARNING: + f(a, a.warn, "dygraphs: " + g); + break; + case Dygraph.ERROR: + f(a, a.error, "dygraphs: " + g); + break + } + } + if (Dygraph.LOG_STACK_TRACES) { + window.console.log(b.join("\n")) + } +}; +Dygraph.info = function(a) { + Dygraph.log(Dygraph.INFO, a) +}; +Dygraph.prototype.info = Dygraph.info; +Dygraph.warn = function(a) { + Dygraph.log(Dygraph.WARNING, a) +}; +Dygraph.prototype.warn = Dygraph.warn; +Dygraph.error = function(a) { + Dygraph.log(Dygraph.ERROR, a) +}; +Dygraph.prototype.error = Dygraph.error; +Dygraph.getContext = function(a) { + return (a.getContext("2d")) +}; +Dygraph.addEvent = function addEvent(c, b, a) { + if (c.addEventListener) { + c.addEventListener(b, a, false) + } else { + c[b + a] = function() { + a(window.event) + }; + c.attachEvent("on" + b, c[b + a]) + } +}; +Dygraph.prototype.addAndTrackEvent = function(c, b, a) { + Dygraph.addEvent(c, b, a); + this.registeredEvents_.push({ + elem: c, + type: b, + fn: a + }) +}; +Dygraph.removeEvent = function(c, b, a) { + if (c.removeEventListener) { + c.removeEventListener(b, a, false) + } else { + try { + c.detachEvent("on" + b, c[b + a]) + } catch (d) {} + c[b + a] = null + } +}; +Dygraph.prototype.removeTrackedEvents_ = function() { + if (this.registeredEvents_) { + for (var a = 0; a < this.registeredEvents_.length; a++) { + var b = this.registeredEvents_[a]; + Dygraph.removeEvent(b.elem, b.type, b.fn) + } + } + this.registeredEvents_ = [] +}; +Dygraph.cancelEvent = function(a) { + a = a ? a : window.event; + if (a.stopPropagation) { + a.stopPropagation() + } + if (a.preventDefault) { + a.preventDefault() + } + a.cancelBubble = true; + a.cancel = true; + a.returnValue = false; + return false +}; +Dygraph.hsvToRGB = function(h, g, k) { + var c; + var d; + var l; + if (g === 0) { + c = k; + d = k; + l = k + } else { + var e = Math.floor(h * 6); + var j = (h * 6) - e; + var b = k * (1 - g); + var a = k * (1 - (g * j)); + var m = k * (1 - (g * (1 - j))); + switch (e) { + case 1: + c = a; + d = k; + l = b; + break; + case 2: + c = b; + d = k; + l = m; + break; + case 3: + c = b; + d = a; + l = k; + break; + case 4: + c = m; + d = b; + l = k; + break; + case 5: + c = k; + d = b; + l = a; + break; + case 6: + case 0: + c = k; + d = m; + l = b; + break + } + } + c = Math.floor(255 * c + 0.5); + d = Math.floor(255 * d + 0.5); + l = Math.floor(255 * l + 0.5); + return "rgb(" + c + "," + d + "," + l + ")" +}; +Dygraph.findPosX = function(c) { + var d = 0; + if (c.offsetParent) { + var a = c; + while (1) { + var b = "0"; + if (window.getComputedStyle) { + b = window.getComputedStyle(a, null).borderLeft || "0" + } + d += parseInt(b, 10); + d += a.offsetLeft; + if (!a.offsetParent) { + break + } + a = a.offsetParent + } + } else { + if (c.x) { + d += c.x + } + } + while (c && c != document.body) { + d -= c.scrollLeft; + c = c.parentNode + } + return d +}; +Dygraph.findPosY = function(c) { + var b = 0; + if (c.offsetParent) { + var a = c; + while (1) { + var d = "0"; + if (window.getComputedStyle) { + d = window.getComputedStyle(a, null).borderTop || "0" + } + b += parseInt(d, 10); + b += a.offsetTop; + if (!a.offsetParent) { + break + } + a = a.offsetParent + } + } else { + if (c.y) { + b += c.y + } + } + while (c && c != document.body) { + b -= c.scrollTop; + c = c.parentNode + } + return b +}; +Dygraph.pageX = function(c) { + if (c.pageX) { + return (!c.pageX || c.pageX < 0) ? 0 : c.pageX + } else { + var d = document.documentElement; + var a = document.body; + return c.clientX + (d.scrollLeft || a.scrollLeft) - (d.clientLeft || 0) + } +}; +Dygraph.pageY = function(c) { + if (c.pageY) { + return (!c.pageY || c.pageY < 0) ? 0 : c.pageY + } else { + var d = document.documentElement; + var a = document.body; + return c.clientY + (d.scrollTop || a.scrollTop) - (d.clientTop || 0) + } +}; +Dygraph.isOK = function(a) { + return !!a && !isNaN(a) +}; +Dygraph.isValidPoint = function(b, a) { + if (!b) { + return false + } + if (b.yval === null) { + return false + } + if (b.x === null || b.x === undefined) { + return false + } + if (b.y === null || b.y === undefined) { + return false + } + if (isNaN(b.x) || (!a && isNaN(b.y))) { + return false + } + return true +}; +Dygraph.floatFormat = function(a, b) { + var c = Math.min(Math.max(1, b || 2), 21); + return (Math.abs(a) < 0.001 && a !== 0) ? a.toExponential(c - 1) : a.toPrecision(c) +}; +Dygraph.zeropad = function(a) { + if (a < 10) { + return "0" + a + } else { + return "" + a + } +}; +Dygraph.hmsString_ = function(a) { + var c = Dygraph.zeropad; + var b = new Date(a); + if (b.getSeconds()) { + return c(b.getHours()) + ":" + c(b.getMinutes()) + ":" + c(b.getSeconds()) + } else { + return c(b.getHours()) + ":" + c(b.getMinutes()) + } +}; +Dygraph.round_ = function(c, b) { + var a = Math.pow(10, b); + return Math.round(c * a) / a +}; +Dygraph.binarySearch = function(a, d, i, e, b) { + if (e === null || e === undefined || b === null || b === undefined) { + e = 0; + b = d.length - 1 + } + if (e > b) { + return -1 + } + if (i === null || i === undefined) { + i = 0 + } + var h = function(j) { + return j >= 0 && j < d.length + }; + var g = parseInt((e + b) / 2, 10); + var c = d[g]; + var f; + if (c == a) { + return g + } else { + if (c > a) { + if (i > 0) { + f = g - 1; + if (h(f) && d[f] < a) { + return g + } + } + return Dygraph.binarySearch(a, d, i, e, g - 1) + } else { + if (c < a) { + if (i < 0) { + f = g + 1; + if (h(f) && d[f] > a) { + return g + } + } + return Dygraph.binarySearch(a, d, i, g + 1, b) + } + } + } + return -1 +}; +Dygraph.dateParser = function(a) { + var b; + var c; + if (a.search("-") == -1 || a.search("T") != -1 || a.search("Z") != -1) { + c = Dygraph.dateStrToMillis(a); + if (c && !isNaN(c)) { + return c + } + } + if (a.search("-") != -1) { + b = a.replace("-", "/", "g"); + while (b.search("-") != -1) { + b = b.replace("-", "/") + } + c = Dygraph.dateStrToMillis(b) + } else { + if (a.length == 8) { + b = a.substr(0, 4) + "/" + a.substr(4, 2) + "/" + a.substr(6, 2); + c = Dygraph.dateStrToMillis(b) + } else { + c = Dygraph.dateStrToMillis(a) + } + } + if (!c || isNaN(c)) { + Dygraph.error("Couldn't parse " + a + " as a date") + } + return c +}; +Dygraph.dateStrToMillis = function(a) { + return new Date(a).getTime() +}; +Dygraph.update = function(b, c) { + if (typeof(c) != "undefined" && c !== null) { + for (var a in c) { + if (c.hasOwnProperty(a)) { + b[a] = c[a] + } + } + } + return b +}; +Dygraph.updateDeep = function(b, d) { + function c(e) { + return (typeof Node === "object" ? e instanceof Node : typeof e === "object" && typeof e.nodeType === "number" && typeof e.nodeName === "string") + } + if (typeof(d) != "undefined" && d !== null) { + for (var a in d) { + if (d.hasOwnProperty(a)) { + if (d[a] === null) { + b[a] = null + } else { + if (Dygraph.isArrayLike(d[a])) { + b[a] = d[a].slice() + } else { + if (c(d[a])) { + b[a] = d[a] + } else { + if (typeof(d[a]) == "object") { + if (typeof(b[a]) != "object" || b[a] === null) { + b[a] = {} + } + Dygraph.updateDeep(b[a], d[a]) + } else { + b[a] = d[a] + } + } + } + } + } + } + } + return b +}; +Dygraph.isArrayLike = function(b) { + var a = typeof(b); + if ((a != "object" && !(a == "function" && typeof(b.item) == "function")) || b === null || typeof(b.length) != "number" || b.nodeType === 3) { + return false + } + return true +}; +Dygraph.isDateLike = function(a) { + if (typeof(a) != "object" || a === null || typeof(a.getTime) != "function") { + return false + } + return true +}; +Dygraph.clone = function(c) { + var b = []; + for (var a = 0; a < c.length; a++) { + if (Dygraph.isArrayLike(c[a])) { + b.push(Dygraph.clone(c[a])) + } else { + b.push(c[a]) + } + } + return b +}; +Dygraph.createCanvas = function() { + var a = document.createElement("canvas"); + var b = (/MSIE/.test(navigator.userAgent) && !window.opera); + if (b && (typeof(G_vmlCanvasManager) != "undefined")) { + a = G_vmlCanvasManager.initElement((a)) + } + return a +}; +Dygraph.isAndroid = function() { + return (/Android/).test(navigator.userAgent) +}; +Dygraph.Iterator = function(d, c, b, a) { + c = c || 0; + b = b || d.length; + this.hasNext = true; + this.peek = null; + this.start_ = c; + this.array_ = d; + this.predicate_ = a; + this.end_ = Math.min(d.length, c + b); + this.nextIdx_ = c - 1; + this.next() +}; +Dygraph.Iterator.prototype.next = function() { + if (!this.hasNext) { + return null + } + var c = this.peek; + var b = this.nextIdx_ + 1; + var a = false; + while (b < this.end_) { + if (!this.predicate_ || this.predicate_(this.array_, b)) { + this.peek = this.array_[b]; + a = true; + break + } + b++ + } + this.nextIdx_ = b; + if (!a) { + this.hasNext = false; + this.peek = null + } + return c +}; +Dygraph.createIterator = function(d, c, b, a) { + return new Dygraph.Iterator(d, c, b, a) +}; +Dygraph.requestAnimFrame = (function() { + return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(a) { + window.setTimeout(a, 1000 / 60) + } +})(); +Dygraph.repeatAndCleanup = function(h, g, f, a) { + var i = 0; + var d; + var b = new Date().getTime(); + h(i); + if (g == 1) { + a(); + return + } + var e = g - 1; + (function c() { + if (i >= g) { + return + } + Dygraph.requestAnimFrame.call(window, function() { + var l = new Date().getTime(); + var j = l - b; + d = i; + i = Math.floor(j / f); + var k = i - d; + var m = (i + k) > e; + if (m || (i >= e)) { + h(e); + a() + } else { + if (k !== 0) { + h(i) + } + c() + } + }) + })() +}; +Dygraph.isPixelChangingOptionList = function(h, e) { + var d = { + annotationClickHandler: true, + annotationDblClickHandler: true, + annotationMouseOutHandler: true, + annotationMouseOverHandler: true, + axisLabelColor: true, + axisLineColor: true, + axisLineWidth: true, + clickCallback: true, + digitsAfterDecimal: true, + drawCallback: true, + drawHighlightPointCallback: true, + drawPoints: true, + drawPointCallback: true, + drawXGrid: true, + drawYGrid: true, + fillAlpha: true, + gridLineColor: true, + gridLineWidth: true, + hideOverlayOnMouseOut: true, + highlightCallback: true, + highlightCircleSize: true, + interactionModel: true, + isZoomedIgnoreProgrammaticZoom: true, + labelsDiv: true, + labelsDivStyles: true, + labelsDivWidth: true, + labelsKMB: true, + labelsKMG2: true, + labelsSeparateLines: true, + labelsShowZeroValues: true, + legend: true, + maxNumberWidth: true, + panEdgeFraction: true, + pixelsPerYLabel: true, + pointClickCallback: true, + pointSize: true, + rangeSelectorPlotFillColor: true, + rangeSelectorPlotStrokeColor: true, + showLabelsOnHighlight: true, + showRoller: true, + sigFigs: true, + strokeWidth: true, + underlayCallback: true, + unhighlightCallback: true, + xAxisLabelFormatter: true, + xTicker: true, + xValueFormatter: true, + yAxisLabelFormatter: true, + yValueFormatter: true, + zoomCallback: true + }; + var a = false; + var b = {}; + if (h) { + for (var f = 1; f < h.length; f++) { + b[h[f]] = true + } + } + for (var g in e) { + if (a) { + break + } + if (e.hasOwnProperty(g)) { + if (b[g]) { + for (var c in e[g]) { + if (a) { + break + } + if (e[g].hasOwnProperty(c) && !d[c]) { + a = true + } + } + } else { + if (!d[g]) { + a = true + } + } + } + } + return a +}; +Dygraph.compareArrays = function(c, b) { + if (!Dygraph.isArrayLike(c) || !Dygraph.isArrayLike(b)) { + return false + } + if (c.length !== b.length) { + return false + } + for (var a = 0; a < c.length; a++) { + if (c[a] !== b[a]) { + return false + } + } + return true +}; +Dygraph.regularShape_ = function(o, c, i, f, e, a, n) { + a = a || 0; + n = n || Math.PI * 2 / c; + o.beginPath(); + var g = a; + var d = g; + var h = function() { + var p = f + (Math.sin(d) * i); + var q = e + (-Math.cos(d) * i); + return [p, q] + }; + var b = h(); + var l = b[0]; + var j = b[1]; + o.moveTo(l, j); + for (var m = 0; m < c; m++) { + d = (m == c - 1) ? g : (d + n); + var k = h(); + o.lineTo(k[0], k[1]) + } + o.fill(); + o.stroke() +}; +Dygraph.shapeFunction_ = function(b, a, c) { + return function(j, i, f, e, k, h, d) { + f.strokeStyle = h; + f.fillStyle = "white"; + Dygraph.regularShape_(f, b, d, e, k, a, c) + } +}; +Dygraph.Circles = { + DEFAULT: function(h, f, b, e, d, c, a) { + b.beginPath(); + b.fillStyle = c; + b.arc(e, d, a, 0, 2 * Math.PI, false); + b.fill() + }, + TRIANGLE: Dygraph.shapeFunction_(3), + SQUARE: Dygraph.shapeFunction_(4, Math.PI / 4), + DIAMOND: Dygraph.shapeFunction_(4), + PENTAGON: Dygraph.shapeFunction_(5), + HEXAGON: Dygraph.shapeFunction_(6), + CIRCLE: function(f, e, c, b, h, d, a) { + c.beginPath(); + c.strokeStyle = d; + c.fillStyle = "white"; + c.arc(b, h, a, 0, 2 * Math.PI, false); + c.fill(); + c.stroke() + }, + STAR: Dygraph.shapeFunction_(5, 0, 4 * Math.PI / 5), + PLUS: function(f, e, c, b, h, d, a) { + c.strokeStyle = d; + c.beginPath(); + c.moveTo(b + a, h); + c.lineTo(b - a, h); + c.closePath(); + c.stroke(); + c.beginPath(); + c.moveTo(b, h + a); + c.lineTo(b, h - a); + c.closePath(); + c.stroke() + }, + EX: function(f, e, c, b, h, d, a) { + c.strokeStyle = d; + c.beginPath(); + c.moveTo(b + a, h + a); + c.lineTo(b - a, h - a); + c.closePath(); + c.stroke(); + c.beginPath(); + c.moveTo(b + a, h - a); + c.lineTo(b - a, h + a); + c.closePath(); + c.stroke() + } +}; +Dygraph.IFrameTarp = function() { + this.tarps = [] +}; +Dygraph.IFrameTarp.prototype.cover = function() { + var f = document.getElementsByTagName("iframe"); + for (var c = 0; c < f.length; c++) { + var e = f[c]; + var b = Dygraph.findPosX(e), + h = Dygraph.findPosY(e), + d = e.offsetWidth, + a = e.offsetHeight; + var g = document.createElement("div"); + g.style.position = "absolute"; + g.style.left = b + "px"; + g.style.top = h + "px"; + g.style.width = d + "px"; + g.style.height = a + "px"; + g.style.zIndex = 999; + document.body.appendChild(g); + this.tarps.push(g) + } +}; +Dygraph.IFrameTarp.prototype.uncover = function() { + for (var a = 0; a < this.tarps.length; a++) { + this.tarps[a].parentNode.removeChild(this.tarps[a]) + } + this.tarps = [] +}; +Dygraph.detectLineDelimiter = function(c) { + for (var a = 0; a < c.length; a++) { + var b = c.charAt(a); + if (b === "\r") { + if (((a + 1) < c.length) && (c.charAt(a + 1) === "\n")) { + return "\r\n" + } + return b + } + if (b === "\n") { + if (((a + 1) < c.length) && (c.charAt(a + 1) === "\r")) { + return "\n\r" + } + return b + } + } + return null +}; +Dygraph.isNodeContainedBy = function(b, a) { + if (a === null || b === null) { + return false + } + var c = (b); + while (c && c !== a) { + c = c.parentNode + } + return (c === a) +}; +Dygraph.pow = function(a, b) { + if (b < 0) { + return 1 / Math.pow(a, -b) + } + return Math.pow(a, b) +}; +Dygraph.dateSetters = { + ms: Date.prototype.setMilliseconds, + s: Date.prototype.setSeconds, + m: Date.prototype.setMinutes, + h: Date.prototype.setHours +}; +Dygraph.setDateSameTZ = function(c, b) { + var f = c.getTimezoneOffset(); + for (var a in b) { + if (!b.hasOwnProperty(a)) { + continue + } + var e = Dygraph.dateSetters[a]; + if (!e) { + throw "Invalid setter: " + a + } + e.call(c, b[a]); + if (c.getTimezoneOffset() != f) { + c.setTime(c.getTime() + (f - c.getTimezoneOffset()) * 60 * 1000) + } + } +}; +"use strict"; +Dygraph.GVizChart = function(a) { + this.container = a +}; +Dygraph.GVizChart.prototype.draw = function(b, a) { + this.container.innerHTML = ""; + if (typeof(this.date_graph) != "undefined") { + this.date_graph.destroy() + } + this.date_graph = new Dygraph(this.container, b, a) +}; +Dygraph.GVizChart.prototype.setSelection = function(b) { + var a = false; + if (b.length) { + a = b[0].row + } + this.date_graph.setSelection(a) +}; +Dygraph.GVizChart.prototype.getSelection = function() { + var b = []; + var d = this.date_graph.getSelection(); + if (d < 0) { + return b + } + var a = this.date_graph.layout_.points; + for (var c = 0; c < a.length; ++c) { + b.push({ + row: d, + column: c + 1 + }) + } + return b +}; +"use strict"; +Dygraph.Interaction = {}; +Dygraph.Interaction.startPan = function(o, t, c) { + var r, b; + c.isPanning = true; + var k = t.xAxisRange(); + c.dateRange = k[1] - k[0]; + c.initialLeftmostDate = k[0]; + c.xUnitsPerPixel = c.dateRange / (t.plotter_.area.w - 1); + if (t.attr_("panEdgeFraction")) { + var x = t.width_ * t.attr_("panEdgeFraction"); + var d = t.xAxisExtremes(); + var j = t.toDomXCoord(d[0]) - x; + var l = t.toDomXCoord(d[1]) + x; + var u = t.toDataXCoord(j); + var w = t.toDataXCoord(l); + c.boundedDates = [u, w]; + var f = []; + var a = t.height_ * t.attr_("panEdgeFraction"); + for (r = 0; r < t.axes_.length; r++) { + b = t.axes_[r]; + var p = b.extremeRange; + var q = t.toDomYCoord(p[0], r) + a; + var s = t.toDomYCoord(p[1], r) - a; + var n = t.toDataYCoord(q, r); + var e = t.toDataYCoord(s, r); + f[r] = [n, e] + } + c.boundedValues = f + } + c.is2DPan = false; + c.axes = []; + for (r = 0; r < t.axes_.length; r++) { + b = t.axes_[r]; + var h = {}; + var m = t.yAxisRange(r); + var v = t.attributes_.getForAxis("logscale", r); + if (v) { + h.initialTopValue = Dygraph.log10(m[1]); + h.dragValueRange = Dygraph.log10(m[1]) - Dygraph.log10(m[0]) + } else { + h.initialTopValue = m[1]; + h.dragValueRange = m[1] - m[0] + } + h.unitsPerPixel = h.dragValueRange / (t.plotter_.area.h - 1); + c.axes.push(h); + if (b.valueWindow || b.valueRange) { + c.is2DPan = true + } + } +}; +Dygraph.Interaction.movePan = function(b, k, c) { + c.dragEndX = k.dragGetX_(b, c); + c.dragEndY = k.dragGetY_(b, c); + var h = c.initialLeftmostDate - (c.dragEndX - c.dragStartX) * c.xUnitsPerPixel; + if (c.boundedDates) { + h = Math.max(h, c.boundedDates[0]) + } + var a = h + c.dateRange; + if (c.boundedDates) { + if (a > c.boundedDates[1]) { + h = h - (a - c.boundedDates[1]); + a = h + c.dateRange + } + } + k.dateWindow_ = [h, a]; + if (c.is2DPan) { + var d = c.dragEndY - c.dragStartY; + for (var j = 0; j < k.axes_.length; j++) { + var e = k.axes_[j]; + var o = c.axes[j]; + var p = d * o.unitsPerPixel; + var f = c.boundedValues ? c.boundedValues[j] : null; + var l = o.initialTopValue + p; + if (f) { + l = Math.min(l, f[1]) + } + var n = l - o.dragValueRange; + if (f) { + if (n < f[0]) { + l = l - (n - f[0]); + n = l - o.dragValueRange + } + } + var m = k.attributes_.getForAxis("logscale", j); + if (m) { + e.valueWindow = [Math.pow(Dygraph.LOG_SCALE, n), Math.pow(Dygraph.LOG_SCALE, l)] + } else { + e.valueWindow = [n, l] + } + } + } + k.drawGraph_(false) +}; +Dygraph.Interaction.endPan = function(c, b, a) { + a.dragEndX = b.dragGetX_(c, a); + a.dragEndY = b.dragGetY_(c, a); + var e = Math.abs(a.dragEndX - a.dragStartX); + var d = Math.abs(a.dragEndY - a.dragStartY); + if (e < 2 && d < 2 && b.lastx_ !== undefined && b.lastx_ != -1) { + Dygraph.Interaction.treatMouseOpAsClick(b, c, a) + } + a.isPanning = false; + a.is2DPan = false; + a.initialLeftmostDate = null; + a.dateRange = null; + a.valueRange = null; + a.boundedDates = null; + a.boundedValues = null; + a.axes = null +}; +Dygraph.Interaction.startZoom = function(c, b, a) { + a.isZooming = true; + a.zoomMoved = false +}; +Dygraph.Interaction.moveZoom = function(c, b, a) { + a.zoomMoved = true; + a.dragEndX = b.dragGetX_(c, a); + a.dragEndY = b.dragGetY_(c, a); + var e = Math.abs(a.dragStartX - a.dragEndX); + var d = Math.abs(a.dragStartY - a.dragEndY); + a.dragDirection = (e < d / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL; + b.drawZoomRect_(a.dragDirection, a.dragStartX, a.dragEndX, a.dragStartY, a.dragEndY, a.prevDragDirection, a.prevEndX, a.prevEndY); + a.prevEndX = a.dragEndX; + a.prevEndY = a.dragEndY; + a.prevDragDirection = a.dragDirection +}; +Dygraph.Interaction.treatMouseOpAsClick = function(f, b, d) { + var k = f.attr_("clickCallback"); + var n = f.attr_("pointClickCallback"); + var j = null; + if (n) { + var l = -1; + var m = Number.MAX_VALUE; + for (var e = 0; e < f.selPoints_.length; e++) { + var c = f.selPoints_[e]; + var a = Math.pow(c.canvasx - d.dragEndX, 2) + Math.pow(c.canvasy - d.dragEndY, 2); + if (!isNaN(a) && (l == -1 || a < m)) { + m = a; + l = e + } + } + var h = f.attr_("highlightCircleSize") + 2; + if (m <= h * h) { + j = f.selPoints_[l] + } + } + if (j) { + n(b, j) + } + if (k) { + k(b, f.lastx_, f.selPoints_) + } +}; +Dygraph.Interaction.endZoom = function(c, i, e) { + e.isZooming = false; + e.dragEndX = i.dragGetX_(c, e); + e.dragEndY = i.dragGetY_(c, e); + var h = Math.abs(e.dragEndX - e.dragStartX); + var d = Math.abs(e.dragEndY - e.dragStartY); + if (h < 2 && d < 2 && i.lastx_ !== undefined && i.lastx_ != -1) { + Dygraph.Interaction.treatMouseOpAsClick(i, c, e) + } + var b = i.getArea(); + if (h >= 10 && e.dragDirection == Dygraph.HORIZONTAL) { + var f = Math.min(e.dragStartX, e.dragEndX), + k = Math.max(e.dragStartX, e.dragEndX); + f = Math.max(f, b.x); + k = Math.min(k, b.x + b.w); + if (f < k) { + i.doZoomX_(f, k) + } + e.cancelNextDblclick = true + } else { + if (d >= 10 && e.dragDirection == Dygraph.VERTICAL) { + var j = Math.min(e.dragStartY, e.dragEndY), + a = Math.max(e.dragStartY, e.dragEndY); + j = Math.max(j, b.y); + a = Math.min(a, b.y + b.h); + if (j < a) { + i.doZoomY_(j, a) + } + e.cancelNextDblclick = true + } else { + if (e.zoomMoved) { + i.clearZoomRect_() + } + } + } + e.dragStartX = null; + e.dragStartY = null +}; +Dygraph.Interaction.startTouch = function(f, e, d) { + f.preventDefault(); + if (f.touches.length > 1) { + d.startTimeForDoubleTapMs = null + } + var h = []; + for (var c = 0; c < f.touches.length; c++) { + var b = f.touches[c]; + h.push({ + pageX: b.pageX, + pageY: b.pageY, + dataX: e.toDataXCoord(b.pageX), + dataY: e.toDataYCoord(b.pageY) + }) + } + d.initialTouches = h; + if (h.length == 1) { + d.initialPinchCenter = h[0]; + d.touchDirections = { + x: true, + y: true + } + } else { + if (h.length >= 2) { + d.initialPinchCenter = { + pageX: 0.5 * (h[0].pageX + h[1].pageX), + pageY: 0.5 * (h[0].pageY + h[1].pageY), + dataX: 0.5 * (h[0].dataX + h[1].dataX), + dataY: 0.5 * (h[0].dataY + h[1].dataY) + }; + var a = 180 / Math.PI * Math.atan2(d.initialPinchCenter.pageY - h[0].pageY, h[0].pageX - d.initialPinchCenter.pageX); + a = Math.abs(a); + if (a > 90) { + a = 90 - a + } + d.touchDirections = { + x: (a < (90 - 45 / 2)), + y: (a > 45 / 2) + } + } + } + d.initialRange = { + x: e.xAxisRange(), + y: e.yAxisRange() + } +}; +Dygraph.Interaction.moveTouch = function(n, q, d) { + d.startTimeForDoubleTapMs = null; + var p, l = []; + for (p = 0; p < n.touches.length; p++) { + var k = n.touches[p]; + l.push({ + pageX: k.pageX, + pageY: k.pageY + }) + } + var a = d.initialTouches; + var h; + var j = d.initialPinchCenter; + if (l.length == 1) { + h = l[0] + } else { + h = { + pageX: 0.5 * (l[0].pageX + l[1].pageX), + pageY: 0.5 * (l[0].pageY + l[1].pageY) + } + } + var m = { + pageX: h.pageX - j.pageX, + pageY: h.pageY - j.pageY + }; + var f = d.initialRange.x[1] - d.initialRange.x[0]; + var o = d.initialRange.y[0] - d.initialRange.y[1]; + m.dataX = (m.pageX / q.plotter_.area.w) * f; + m.dataY = (m.pageY / q.plotter_.area.h) * o; + var w, c; + if (l.length == 1) { + w = 1; + c = 1 + } else { + if (l.length >= 2) { + var e = (a[1].pageX - j.pageX); + w = (l[1].pageX - h.pageX) / e; + var v = (a[1].pageY - j.pageY); + c = (l[1].pageY - h.pageY) / v + } + } + w = Math.min(8, Math.max(0.125, w)); + c = Math.min(8, Math.max(0.125, c)); + var u = false; + if (d.touchDirections.x) { + q.dateWindow_ = [j.dataX - m.dataX + (d.initialRange.x[0] - j.dataX) / w, j.dataX - m.dataX + (d.initialRange.x[1] - j.dataX) / w]; + u = true + } + if (d.touchDirections.y) { + for (p = 0; p < 1; p++) { + var b = q.axes_[p]; + var s = q.attributes_.getForAxis("logscale", p); + if (s) {} else { + b.valueWindow = [j.dataY - m.dataY + (d.initialRange.y[0] - j.dataY) / c, j.dataY - m.dataY + (d.initialRange.y[1] - j.dataY) / c]; + u = true + } + } + } + q.drawGraph_(false); + if (u && l.length > 1 && q.attr_("zoomCallback")) { + var r = q.xAxisRange(); + q.attr_("zoomCallback")(r[0], r[1], q.yAxisRanges()) + } +}; +Dygraph.Interaction.endTouch = function(e, d, c) { + if (e.touches.length !== 0) { + Dygraph.Interaction.startTouch(e, d, c) + } else { + if (e.changedTouches.length == 1) { + var a = new Date().getTime(); + var b = e.changedTouches[0]; + if (c.startTimeForDoubleTapMs && a - c.startTimeForDoubleTapMs < 500 && c.doubleTapX && Math.abs(c.doubleTapX - b.screenX) < 50 && c.doubleTapY && Math.abs(c.doubleTapY - b.screenY) < 50) { + d.resetZoom() + } else { + c.startTimeForDoubleTapMs = a; + c.doubleTapX = b.screenX; + c.doubleTapY = b.screenY + } + } + } +}; +Dygraph.Interaction.defaultModel = { + mousedown: function(c, b, a) { + if (c.button && c.button == 2) { + return + } + a.initializeMouseDown(c, b, a); + if (c.altKey || c.shiftKey) { + Dygraph.startPan(c, b, a) + } else { + Dygraph.startZoom(c, b, a) + } + }, + mousemove: function(c, b, a) { + if (a.isZooming) { + Dygraph.moveZoom(c, b, a) + } else { + if (a.isPanning) { + Dygraph.movePan(c, b, a) + } + } + }, + mouseup: function(c, b, a) { + if (a.isZooming) { + Dygraph.endZoom(c, b, a) + } else { + if (a.isPanning) { + Dygraph.endPan(c, b, a) + } + } + }, + touchstart: function(c, b, a) { + Dygraph.Interaction.startTouch(c, b, a) + }, + touchmove: function(c, b, a) { + Dygraph.Interaction.moveTouch(c, b, a) + }, + touchend: function(c, b, a) { + Dygraph.Interaction.endTouch(c, b, a) + }, + mouseout: function(c, b, a) { + if (a.isZooming) { + a.dragEndX = null; + a.dragEndY = null; + b.clearZoomRect_() + } + }, + dblclick: function(c, b, a) { + if (a.cancelNextDblclick) { + a.cancelNextDblclick = false; + return + } + if (c.altKey || c.shiftKey) { + return + } + b.resetZoom() + } +}; +Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.Interaction.defaultModel; +Dygraph.defaultInteractionModel = Dygraph.Interaction.defaultModel; +Dygraph.endZoom = Dygraph.Interaction.endZoom; +Dygraph.moveZoom = Dygraph.Interaction.moveZoom; +Dygraph.startZoom = Dygraph.Interaction.startZoom; +Dygraph.endPan = Dygraph.Interaction.endPan; +Dygraph.movePan = Dygraph.Interaction.movePan; +Dygraph.startPan = Dygraph.Interaction.startPan; +Dygraph.Interaction.nonInteractiveModel_ = { + mousedown: function(c, b, a) { + a.initializeMouseDown(c, b, a) + }, + mouseup: function(c, b, a) { + a.dragEndX = b.dragGetX_(c, a); + a.dragEndY = b.dragGetY_(c, a); + var e = Math.abs(a.dragEndX - a.dragStartX); + var d = Math.abs(a.dragEndY - a.dragStartY); + if (e < 2 && d < 2 && b.lastx_ !== undefined && b.lastx_ != -1) { + Dygraph.Interaction.treatMouseOpAsClick(b, c, a) + } + } +}; +Dygraph.Interaction.dragIsPanInteractionModel = { + mousedown: function(c, b, a) { + a.initializeMouseDown(c, b, a); + Dygraph.startPan(c, b, a) + }, + mousemove: function(c, b, a) { + if (a.isPanning) { + Dygraph.movePan(c, b, a) + } + }, + mouseup: function(c, b, a) { + if (a.isPanning) { + Dygraph.endPan(c, b, a) + } + } +}; +"use strict"; +Dygraph.TickList = undefined; +Dygraph.Ticker = undefined; +Dygraph.numericLinearTicks = function(d, c, i, g, f, h) { + var e = function(a) { + if (a === "logscale") { + return false + } + return g(a) + }; + return Dygraph.numericTicks(d, c, i, e, f, h) +}; +Dygraph.numericTicks = function(F, E, u, p, d, q) { + var z = (p("pixelsPerLabel")); + var G = []; + var C, A, t, y; + if (q) { + for (C = 0; C < q.length; C++) { + G.push({ + v: q[C] + }) + } + } else { + if (p("logscale")) { + y = Math.floor(u / z); + var l = Dygraph.binarySearch(F, Dygraph.PREFERRED_LOG_TICK_VALUES, 1); + var H = Dygraph.binarySearch(E, Dygraph.PREFERRED_LOG_TICK_VALUES, -1); + if (l == -1) { + l = 0 + } + if (H == -1) { + H = Dygraph.PREFERRED_LOG_TICK_VALUES.length - 1 + } + var s = null; + if (H - l >= y / 4) { + for (var r = H; r >= l; r--) { + var m = Dygraph.PREFERRED_LOG_TICK_VALUES[r]; + var k = Math.log(m / F) / Math.log(E / F) * u; + var D = { + v: m + }; + if (s === null) { + s = { + tickValue: m, + pixel_coord: k + } + } else { + if (Math.abs(k - s.pixel_coord) >= z) { + s = { + tickValue: m, + pixel_coord: k + } + } else { + D.label = "" + } + } + G.push(D) + } + G.reverse() + } + } + if (G.length === 0) { + var g = p("labelsKMG2"); + var n, h; + if (g) { + n = [1, 2, 4, 8, 16, 32, 64, 128, 256]; + h = 16 + } else { + n = [1, 2, 5, 10, 20, 50, 100]; + h = 10 + } + var w = Math.ceil(u / z); + var o = Math.abs(E - F) / w; + var v = Math.floor(Math.log(o) / Math.log(h)); + var f = Math.pow(h, v); + var I, x, c, e; + for (A = 0; A < n.length; A++) { + I = f * n[A]; + x = Math.floor(F / I) * I; + c = Math.ceil(E / I) * I; + y = Math.abs(c - x) / I; + e = u / y; + if (e > z) { + break + } + } + if (x > c) { + I *= -1 + } + for (C = 0; C < y; C++) { + t = x + C * I; + G.push({ + v: t + }) + } + } + } + var B = (p("axisLabelFormatter")); + for (C = 0; C < G.length; C++) { + if (G[C].label !== undefined) { + continue + } + G[C].label = B(G[C].v, 0, p, d) + } + return G +}; +Dygraph.dateTicker = function(e, c, i, g, f, h) { + var d = Dygraph.pickDateTickGranularity(e, c, i, g); + if (d >= 0) { + return Dygraph.getDateAxis(e, c, d, g, f) + } else { + return [] + } +}; +Dygraph.SECONDLY = 0; +Dygraph.TWO_SECONDLY = 1; +Dygraph.FIVE_SECONDLY = 2; +Dygraph.TEN_SECONDLY = 3; +Dygraph.THIRTY_SECONDLY = 4; +Dygraph.MINUTELY = 5; +Dygraph.TWO_MINUTELY = 6; +Dygraph.FIVE_MINUTELY = 7; +Dygraph.TEN_MINUTELY = 8; +Dygraph.THIRTY_MINUTELY = 9; +Dygraph.HOURLY = 10; +Dygraph.TWO_HOURLY = 11; +Dygraph.SIX_HOURLY = 12; +Dygraph.DAILY = 13; +Dygraph.WEEKLY = 14; +Dygraph.MONTHLY = 15; +Dygraph.QUARTERLY = 16; +Dygraph.BIANNUAL = 17; +Dygraph.ANNUAL = 18; +Dygraph.DECADAL = 19; +Dygraph.CENTENNIAL = 20; +Dygraph.NUM_GRANULARITIES = 21; +Dygraph.SHORT_SPACINGS = []; +Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY] = 1000 * 1; +Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY] = 1000 * 2; +Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY] = 1000 * 5; +Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY] = 1000 * 10; +Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30; +Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY] = 1000 * 60; +Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY] = 1000 * 60 * 2; +Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY] = 1000 * 60 * 5; +Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY] = 1000 * 60 * 10; +Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30; +Dygraph.SHORT_SPACINGS[Dygraph.HOURLY] = 1000 * 3600; +Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY] = 1000 * 3600 * 2; +Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY] = 1000 * 3600 * 6; +Dygraph.SHORT_SPACINGS[Dygraph.DAILY] = 1000 * 86400; +Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY] = 1000 * 604800; +Dygraph.LONG_TICK_PLACEMENTS = []; +Dygraph.LONG_TICK_PLACEMENTS[Dygraph.MONTHLY] = { + months: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + year_mod: 1 +}; +Dygraph.LONG_TICK_PLACEMENTS[Dygraph.QUARTERLY] = { + months: [0, 3, 6, 9], + year_mod: 1 +}; +Dygraph.LONG_TICK_PLACEMENTS[Dygraph.BIANNUAL] = { + months: [0, 6], + year_mod: 1 +}; +Dygraph.LONG_TICK_PLACEMENTS[Dygraph.ANNUAL] = { + months: [0], + year_mod: 1 +}; +Dygraph.LONG_TICK_PLACEMENTS[Dygraph.DECADAL] = { + months: [0], + year_mod: 10 +}; +Dygraph.LONG_TICK_PLACEMENTS[Dygraph.CENTENNIAL] = { + months: [0], + year_mod: 100 +}; +Dygraph.PREFERRED_LOG_TICK_VALUES = function() { + var c = []; + for (var b = -39; b <= 39; b++) { + var a = Math.pow(10, b); + for (var d = 1; d <= 9; d++) { + var e = a * d; + c.push(e) + } + } + return c +}(); +Dygraph.pickDateTickGranularity = function(d, c, j, h) { + var g = (h("pixelsPerLabel")); + for (var f = 0; f < Dygraph.NUM_GRANULARITIES; f++) { + var e = Dygraph.numDateTicks(d, c, f); + if (j / e >= g) { + return f + } + } + return -1 +}; +Dygraph.numDateTicks = function(e, b, f) { + if (f < Dygraph.MONTHLY) { + var g = Dygraph.SHORT_SPACINGS[f]; + return Math.floor(0.5 + 1 * (b - e) / g) + } else { + var d = Dygraph.LONG_TICK_PLACEMENTS[f]; + var c = 365.2524 * 24 * 3600 * 1000; + var a = 1 * (b - e) / c; + return Math.floor(0.5 + 1 * a * d.months.length / d.year_mod) + } +}; +Dygraph.getDateAxis = function(p, l, a, n, z) { + var w = (n("axisLabelFormatter")); + var C = []; + var m; + if (a < Dygraph.MONTHLY) { + var c = Dygraph.SHORT_SPACINGS[a]; + var y = c / 1000; + var A = new Date(p); + Dygraph.setDateSameTZ(A, { + ms: 0 + }); + var h; + if (y <= 60) { + h = A.getSeconds(); + Dygraph.setDateSameTZ(A, { + s: h - h % y + }) + } else { + Dygraph.setDateSameTZ(A, { + s: 0 + }); + y /= 60; + if (y <= 60) { + h = A.getMinutes(); + Dygraph.setDateSameTZ(A, { + m: h - h % y + }) + } else { + Dygraph.setDateSameTZ(A, { + m: 0 + }); + y /= 60; + if (y <= 24) { + h = A.getHours(); + A.setHours(h - h % y) + } else { + A.setHours(0); + y /= 24; + if (y == 7) { + A.setDate(A.getDate() - A.getDay()) + } + } + } + } + p = A.getTime(); + var B = new Date(p).getTimezoneOffset(); + var e = (c >= Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]); + for (m = p; m <= l; m += c) { + A = new Date(m); + if (e && A.getTimezoneOffset() != B) { + var k = A.getTimezoneOffset() - B; + m += k * 60 * 1000; + A = new Date(m); + B = A.getTimezoneOffset(); + if (new Date(m + c).getTimezoneOffset() != B) { + m += c; + A = new Date(m); + B = A.getTimezoneOffset() + } + } + C.push({ + v: m, + label: w(A, a, n, z) + }) + } + } else { + var f; + var r = 1; + if (a < Dygraph.NUM_GRANULARITIES) { + f = Dygraph.LONG_TICK_PLACEMENTS[a].months; + r = Dygraph.LONG_TICK_PLACEMENTS[a].year_mod + } else { + Dygraph.warn("Span of dates is too long") + } + var v = new Date(p).getFullYear(); + var q = new Date(l).getFullYear(); + var b = Dygraph.zeropad; + for (var u = v; u <= q; u++) { + if (u % r !== 0) { + continue + } + for (var s = 0; s < f.length; s++) { + var o = u + "/" + b(1 + f[s]) + "/01"; + m = Dygraph.dateStrToMillis(o); + if (m < p || m > l) { + continue + } + C.push({ + v: m, + label: w(new Date(m), a, n, z) + }) + } + } + } + return C +}; +if (Dygraph && Dygraph.DEFAULT_ATTRS && Dygraph.DEFAULT_ATTRS.axes && Dygraph.DEFAULT_ATTRS.axes["x"] && Dygraph.DEFAULT_ATTRS.axes["y"] && Dygraph.DEFAULT_ATTRS.axes["y2"]) { + Dygraph.DEFAULT_ATTRS.axes["x"]["ticker"] = Dygraph.dateTicker; + Dygraph.DEFAULT_ATTRS.axes["y"]["ticker"] = Dygraph.numericTicks; + Dygraph.DEFAULT_ATTRS.axes["y2"]["ticker"] = Dygraph.numericTicks +} +Dygraph.Plugins = {}; +Dygraph.Plugins.Annotations = (function() { + var a = function() { + this.annotations_ = [] + }; + a.prototype.toString = function() { + return "Annotations Plugin" + }; + a.prototype.activate = function(b) { + return { + clearChart: this.clearChart, + didDrawChart: this.didDrawChart + } + }; + a.prototype.detachLabels = function() { + for (var c = 0; c < this.annotations_.length; c++) { + var b = this.annotations_[c]; + if (b.parentNode) { + b.parentNode.removeChild(b) + } + this.annotations_[c] = null + } + this.annotations_ = [] + }; + a.prototype.clearChart = function(b) { + this.detachLabels() + }; + a.prototype.didDrawChart = function(v) { + var t = v.dygraph; + var r = t.layout_.annotated_points; + if (!r || r.length === 0) { + return + } + var h = v.canvas.parentNode; + var x = { + position: "absolute", + fontSize: t.getOption("axisLabelFontSize") + "px", + zIndex: 10, + overflow: "hidden" + }; + var b = function(e, g, i) { + return function(y) { + var p = i.annotation; + if (p.hasOwnProperty(e)) { + p[e](p, i, t, y) + } else { + if (t.getOption(g)) { + t.getOption(g)(p, i, t, y) + } + } + } + }; + var u = v.dygraph.plotter_.area; + var q = {}; + for (var s = 0; s < r.length; s++) { + var l = r[s]; + if (l.canvasx < u.x || l.canvasx > u.x + u.w || l.canvasy < u.y || l.canvasy > u.y + u.h) { + continue + } + var w = l.annotation; + var n = 6; + if (w.hasOwnProperty("tickHeight")) { + n = w.tickHeight + } + var j = document.createElement("div"); + for (var A in x) { + if (x.hasOwnProperty(A)) { + j.style[A] = x[A] + } + } + if (!w.hasOwnProperty("icon")) { + j.className = "dygraphDefaultAnnotation" + } + if (w.hasOwnProperty("cssClass")) { + j.className += " " + w.cssClass + } + var m = w.hasOwnProperty("width") ? w.width : 16; + var k = w.hasOwnProperty("height") ? w.height : 16; + if (w.hasOwnProperty("icon")) { + var z = document.createElement("img"); + z.src = w.icon; + z.width = m; + z.height = k; + j.appendChild(z) + } else { + if (l.annotation.hasOwnProperty("shortText")) { + j.appendChild(document.createTextNode(l.annotation.shortText)) + } + } + var c = l.canvasx - m / 2; + j.style.left = c + "px"; + var f = 0; + if (w.attachAtBottom) { + var d = (u.y + u.h - k - n); + if (q[c]) { + d -= q[c] + } else { + q[c] = 0 + } + q[c] += (n + k); + f = d + } else { + f = l.canvasy - k - n + } + j.style.top = f + "px"; + j.style.width = m + "px"; + j.style.height = k + "px"; + j.title = l.annotation.text; + j.style.color = t.colorsMap_[l.name]; + j.style.borderColor = t.colorsMap_[l.name]; + w.div = j; + t.addAndTrackEvent(j, "click", b("clickHandler", "annotationClickHandler", l, this)); + t.addAndTrackEvent(j, "mouseover", b("mouseOverHandler", "annotationMouseOverHandler", l, this)); + t.addAndTrackEvent(j, "mouseout", b("mouseOutHandler", "annotationMouseOutHandler", l, this)); + t.addAndTrackEvent(j, "dblclick", b("dblClickHandler", "annotationDblClickHandler", l, this)); + h.appendChild(j); + this.annotations_.push(j); + var o = v.drawingContext; + o.save(); + o.strokeStyle = t.colorsMap_[l.name]; + o.beginPath(); + if (!w.attachAtBottom) { + o.moveTo(l.canvasx, l.canvasy); + o.lineTo(l.canvasx, l.canvasy - 2 - n) + } else { + var d = f + k; + o.moveTo(l.canvasx, d); + o.lineTo(l.canvasx, d + n) + } + o.closePath(); + o.stroke(); + o.restore() + } + }; + a.prototype.destroy = function() { + this.detachLabels() + }; + return a +})(); +Dygraph.Plugins.Axes = (function() { + var a = function() { + this.xlabels_ = []; + this.ylabels_ = [] + }; + a.prototype.toString = function() { + return "Axes Plugin" + }; + a.prototype.activate = function(b) { + return { + layout: this.layout, + clearChart: this.clearChart, + willDrawChart: this.willDrawChart + } + }; + a.prototype.layout = function(f) { + var d = f.dygraph; + if (d.getOption("drawYAxis")) { + var b = d.getOption("yAxisLabelWidth") + 2 * d.getOption("axisTickSize"); + f.reserveSpaceLeft(b) + } + if (d.getOption("drawXAxis")) { + var c; + if (d.getOption("xAxisHeight")) { + c = d.getOption("xAxisHeight") + } else { + c = d.getOptionForAxis("axisLabelFontSize", "x") + 2 * d.getOption("axisTickSize") + } + f.reserveSpaceBottom(c) + } + if (d.numAxes() == 2) { + if (d.getOption("drawYAxis")) { + var b = d.getOption("yAxisLabelWidth") + 2 * d.getOption("axisTickSize"); + f.reserveSpaceRight(b) + } + } else { + if (d.numAxes() > 2) { + d.error("Only two y-axes are supported at this time. (Trying to use " + d.numAxes() + ")") + } + } + }; + a.prototype.detachLabels = function() { + function b(d) { + for (var c = 0; c < d.length; c++) { + var e = d[c]; + if (e.parentNode) { + e.parentNode.removeChild(e) + } + } + } + b(this.xlabels_); + b(this.ylabels_); + this.xlabels_ = []; + this.ylabels_ = [] + }; + a.prototype.clearChart = function(b) { + this.detachLabels() + }; + a.prototype.willDrawChart = function(H) { + var F = H.dygraph; + if (!F.getOption("drawXAxis") && !F.getOption("drawYAxis")) { + return + } + + function B(e) { + return Math.round(e) + 0.5 + } + + function A(e) { + return Math.round(e) - 0.5 + } + var j = H.drawingContext; + var v = H.canvas.parentNode; + var J = H.canvas.width; + var d = H.canvas.height; + var s, u, t, E, D; + var C = function(e) { + return { + position: "absolute", + fontSize: F.getOptionForAxis("axisLabelFontSize", e) + "px", + zIndex: 10, + color: F.getOptionForAxis("axisLabelColor", e), + width: F.getOption("axisLabelWidth") + "px", + lineHeight: "normal", + overflow: "hidden" + } + }; + var p = { + x: C("x"), + y: C("y"), + y2: C("y2") + }; + var m = function(g, x, y) { + var K = document.createElement("div"); + var e = p[y == "y2" ? "y2" : x]; + for (var r in e) { + if (e.hasOwnProperty(r)) { + K.style[r] = e[r] + } + } + var i = document.createElement("div"); + i.className = "dygraph-axis-label dygraph-axis-label-" + x + (y ? " dygraph-axis-label-" + y : ""); + i.innerHTML = g; + K.appendChild(i); + return K + }; + j.save(); + var I = F.layout_; + var G = H.dygraph.plotter_.area; + if (F.getOption("drawYAxis")) { + if (I.yticks && I.yticks.length > 0) { + var h = F.numAxes(); + for (D = 0; D < I.yticks.length; D++) { + E = I.yticks[D]; + if (typeof(E) == "function") { + return + } + u = G.x; + var o = 1; + var f = "y1"; + if (E[0] == 1) { + u = G.x + G.w; + o = -1; + f = "y2" + } + var k = F.getOptionForAxis("axisLabelFontSize", f); + t = G.y + E[1] * G.h; + s = m(E[2], "y", h == 2 ? f : null); + var z = (t - k / 2); + if (z < 0) { + z = 0 + } + if (z + k + 3 > d) { + s.style.bottom = "0px" + } else { + s.style.top = z + "px" + } + if (E[0] === 0) { + s.style.left = (G.x - F.getOption("yAxisLabelWidth") - F.getOption("axisTickSize")) + "px"; + s.style.textAlign = "right" + } else { + if (E[0] == 1) { + s.style.left = (G.x + G.w + F.getOption("axisTickSize")) + "px"; + s.style.textAlign = "left" + } + } + s.style.width = F.getOption("yAxisLabelWidth") + "px"; + v.appendChild(s); + this.ylabels_.push(s) + } + var n = this.ylabels_[0]; + var k = F.getOptionForAxis("axisLabelFontSize", "y"); + var q = parseInt(n.style.top, 10) + k; + if (q > d - k) { + n.style.top = (parseInt(n.style.top, 10) - k / 2) + "px" + } + } + var c; + if (F.getOption("drawAxesAtZero")) { + var w = F.toPercentXCoord(0); + if (w > 1 || w < 0 || isNaN(w)) { + w = 0 + } + c = B(G.x + w * G.w) + } else { + c = B(G.x) + } + j.strokeStyle = F.getOptionForAxis("axisLineColor", "y"); + j.lineWidth = F.getOptionForAxis("axisLineWidth", "y"); + j.beginPath(); + j.moveTo(c, A(G.y)); + j.lineTo(c, A(G.y + G.h)); + j.closePath(); + j.stroke(); + if (F.numAxes() == 2) { + j.strokeStyle = F.getOptionForAxis("axisLineColor", "y2"); + j.lineWidth = F.getOptionForAxis("axisLineWidth", "y2"); + j.beginPath(); + j.moveTo(A(G.x + G.w), A(G.y)); + j.lineTo(A(G.x + G.w), A(G.y + G.h)); + j.closePath(); + j.stroke() + } + } + if (F.getOption("drawXAxis")) { + if (I.xticks) { + for (D = 0; D < I.xticks.length; D++) { + E = I.xticks[D]; + u = G.x + E[0] * G.w; + t = G.y + G.h; + s = m(E[1], "x"); + s.style.textAlign = "center"; + s.style.top = (t + F.getOption("axisTickSize")) + "px"; + var l = (u - F.getOption("axisLabelWidth") / 2); + if (l + F.getOption("axisLabelWidth") > J) { + l = J - F.getOption("xAxisLabelWidth"); + s.style.textAlign = "right" + } + if (l < 0) { + l = 0; + s.style.textAlign = "left" + } + s.style.left = l + "px"; + s.style.width = F.getOption("xAxisLabelWidth") + "px"; + v.appendChild(s); + this.xlabels_.push(s) + } + } + j.strokeStyle = F.getOptionForAxis("axisLineColor", "x"); + j.lineWidth = F.getOptionForAxis("axisLineWidth", "x"); + j.beginPath(); + var b; + if (F.getOption("drawAxesAtZero")) { + var w = F.toPercentYCoord(0, 0); + if (w > 1 || w < 0) { + w = 1 + } + b = A(G.y + w * G.h) + } else { + b = A(G.y + G.h) + } + j.moveTo(B(G.x), b); + j.lineTo(B(G.x + G.w), b); + j.closePath(); + j.stroke() + } + j.restore() + }; + return a +})(); +Dygraph.Plugins.ChartLabels = (function() { + var c = function() { + this.title_div_ = null; + this.xlabel_div_ = null; + this.ylabel_div_ = null; + this.y2label_div_ = null + }; + c.prototype.toString = function() { + return "ChartLabels Plugin" + }; + c.prototype.activate = function(d) { + return { + layout: this.layout, + didDrawChart: this.didDrawChart + } + }; + var b = function(d) { + var e = document.createElement("div"); + e.style.position = "absolute"; + e.style.left = d.x + "px"; + e.style.top = d.y + "px"; + e.style.width = d.w + "px"; + e.style.height = d.h + "px"; + return e + }; + c.prototype.detachLabels_ = function() { + var e = [this.title_div_, this.xlabel_div_, this.ylabel_div_, this.y2label_div_]; + for (var d = 0; d < e.length; d++) { + var f = e[d]; + if (!f) { + continue + } + if (f.parentNode) { + f.parentNode.removeChild(f) + } + } + this.title_div_ = null; + this.xlabel_div_ = null; + this.ylabel_div_ = null; + this.y2label_div_ = null + }; + var a = function(l, i, f, h, j) { + var d = document.createElement("div"); + d.style.position = "absolute"; + if (f == 1) { + d.style.left = "0px" + } else { + d.style.left = i.x + "px" + } + d.style.top = i.y + "px"; + d.style.width = i.w + "px"; + d.style.height = i.h + "px"; + d.style.fontSize = (l.getOption("yLabelWidth") - 2) + "px"; + var m = document.createElement("div"); + m.style.position = "absolute"; + m.style.width = i.h + "px"; + m.style.height = i.w + "px"; + m.style.top = (i.h / 2 - i.w / 2) + "px"; + m.style.left = (i.w / 2 - i.h / 2) + "px"; + m.style.textAlign = "center"; + var e = "rotate(" + (f == 1 ? "-" : "") + "90deg)"; + m.style.transform = e; + m.style.WebkitTransform = e; + m.style.MozTransform = e; + m.style.OTransform = e; + m.style.msTransform = e; + if (typeof(document.documentMode) !== "undefined" && document.documentMode < 9) { + m.style.filter = "progid:DXImageTransform.Microsoft.BasicImage(rotation=" + (f == 1 ? "3" : "1") + ")"; + m.style.left = "0px"; + m.style.top = "0px" + } + var k = document.createElement("div"); + k.className = h; + k.innerHTML = j; + m.appendChild(k); + d.appendChild(m); + return d + }; + c.prototype.layout = function(k) { + this.detachLabels_(); + var i = k.dygraph; + var m = k.chart_div; + if (i.getOption("title")) { + var d = k.reserveSpaceTop(i.getOption("titleHeight")); + this.title_div_ = b(d); + this.title_div_.style.textAlign = "center"; + this.title_div_.style.fontSize = (i.getOption("titleHeight") - 8) + "px"; + this.title_div_.style.fontWeight = "bold"; + this.title_div_.style.zIndex = 10; + var f = document.createElement("div"); + f.className = "dygraph-label dygraph-title"; + f.innerHTML = i.getOption("title"); + this.title_div_.appendChild(f); + m.appendChild(this.title_div_) + } + if (i.getOption("xlabel")) { + var j = k.reserveSpaceBottom(i.getOption("xLabelHeight")); + this.xlabel_div_ = b(j); + this.xlabel_div_.style.textAlign = "center"; + this.xlabel_div_.style.fontSize = (i.getOption("xLabelHeight") - 2) + "px"; + var f = document.createElement("div"); + f.className = "dygraph-label dygraph-xlabel"; + f.innerHTML = i.getOption("xlabel"); + this.xlabel_div_.appendChild(f); + m.appendChild(this.xlabel_div_) + } + if (i.getOption("ylabel")) { + var h = k.reserveSpaceLeft(0); + this.ylabel_div_ = a(i, h, 1, "dygraph-label dygraph-ylabel", i.getOption("ylabel")); + m.appendChild(this.ylabel_div_) + } + if (i.getOption("y2label") && i.numAxes() == 2) { + var l = k.reserveSpaceRight(0); + this.y2label_div_ = a(i, l, 2, "dygraph-label dygraph-y2label", i.getOption("y2label")); + m.appendChild(this.y2label_div_) + } + }; + c.prototype.didDrawChart = function(f) { + var d = f.dygraph; + if (this.title_div_) { + this.title_div_.children[0].innerHTML = d.getOption("title") + } + if (this.xlabel_div_) { + this.xlabel_div_.children[0].innerHTML = d.getOption("xlabel") + } + if (this.ylabel_div_) { + this.ylabel_div_.children[0].children[0].innerHTML = d.getOption("ylabel") + } + if (this.y2label_div_) { + this.y2label_div_.children[0].children[0].innerHTML = d.getOption("y2label") + } + }; + c.prototype.clearChart = function() {}; + c.prototype.destroy = function() { + this.detachLabels_() + }; + return c +})(); +Dygraph.Plugins.Grid = (function() { + var a = function() {}; + a.prototype.toString = function() { + return "Gridline Plugin" + }; + a.prototype.activate = function(b) { + return { + willDrawChart: this.willDrawChart + } + }; + a.prototype.willDrawChart = function(s) { + var q = s.dygraph; + var l = s.drawingContext; + var t = q.layout_; + var r = s.dygraph.plotter_.area; + + function k(e) { + return Math.round(e) + 0.5 + } + + function j(e) { + return Math.round(e) - 0.5 + } + var h, f, p, u; + if (q.getOption("drawYGrid")) { + var o = ["y", "y2"]; + var m = [], + v = [], + b = [], + n = [], + d = []; + for (var p = 0; p < o.length; p++) { + b[p] = q.getOptionForAxis("drawGrid", o[p]); + if (b[p]) { + m[p] = q.getOptionForAxis("gridLineColor", o[p]); + v[p] = q.getOptionForAxis("gridLineWidth", o[p]); + d[p] = q.getOptionForAxis("gridLinePattern", o[p]); + n[p] = d[p] && (d[p].length >= 2) + } + } + u = t.yticks; + l.save(); + for (p = 0; p < u.length; p++) { + var c = u[p][0]; + if (b[c]) { + if (n[c]) { + l.installPattern(d[c]) + } + l.strokeStyle = m[c]; + l.lineWidth = v[c]; + h = k(r.x); + f = j(r.y + u[p][1] * r.h); + l.beginPath(); + l.moveTo(h, f); + l.lineTo(h + r.w, f); + l.closePath(); + l.stroke(); + if (n[c]) { + l.uninstallPattern() + } + } + } + l.restore() + } + if (q.getOption("drawXGrid") && q.getOptionForAxis("drawGrid", "x")) { + u = t.xticks; + l.save(); + var d = q.getOptionForAxis("gridLinePattern", "x"); + var n = d && (d.length >= 2); + if (n) { + l.installPattern(d) + } + l.strokeStyle = q.getOptionForAxis("gridLineColor", "x"); + l.lineWidth = q.getOptionForAxis("gridLineWidth", "x"); + for (p = 0; p < u.length; p++) { + h = k(r.x + u[p][0] * r.w); + f = j(r.y + r.h); + l.beginPath(); + l.moveTo(h, f); + l.lineTo(h, r.y); + l.closePath(); + l.stroke() + } + if (n) { + l.uninstallPattern() + } + l.restore() + } + }; + a.prototype.destroy = function() {}; + return a +})(); +Dygraph.Plugins.Legend = (function() { + var c = function() { + this.legend_div_ = null; + this.is_generated_div_ = false + }; + c.prototype.toString = function() { + return "Legend Plugin" + }; + var a, d; + c.prototype.activate = function(j) { + var m; + var f = j.getOption("labelsDivWidth"); + var l = j.getOption("labelsDiv"); + if (l && null !== l) { + if (typeof(l) == "string" || l instanceof String) { + m = document.getElementById(l) + } else { + m = l + } + } else { + var i = { + position: "absolute", + fontSize: "14px", + zIndex: 10, + width: f + "px", + top: "0px", + left: (j.size().width - f - 2) + "px", + background: "white", + lineHeight: "normal", + textAlign: "left", + overflow: "hidden" + }; + Dygraph.update(i, j.getOption("labelsDivStyles")); + m = document.createElement("div"); + m.className = "dygraph-legend"; + for (var h in i) { + if (!i.hasOwnProperty(h)) { + continue + } + try { + m.style[h] = i[h] + } catch (k) { + this.warn("You are using unsupported css properties for your browser in labelsDivStyles") + } + } + j.graphDiv.appendChild(m); + this.is_generated_div_ = true + } + this.legend_div_ = m; + this.one_em_width_ = 10; + return { + select: this.select, + deselect: this.deselect, + predraw: this.predraw, + didDrawChart: this.didDrawChart + } + }; + var b = function(g) { + var f = document.createElement("span"); + f.setAttribute("style", "margin: 0; padding: 0 0 0 1em; border: 0;"); + g.appendChild(f); + var e = f.offsetWidth; + g.removeChild(f); + return e + }; + c.prototype.select = function(i) { + var h = i.selectedX; + var g = i.selectedPoints; + var f = a(i.dygraph, h, g, this.one_em_width_); + this.legend_div_.innerHTML = f + }; + c.prototype.deselect = function(h) { + var f = b(this.legend_div_); + this.one_em_width_ = f; + var g = a(h.dygraph, undefined, undefined, f); + this.legend_div_.innerHTML = g + }; + c.prototype.didDrawChart = function(f) { + this.deselect(f) + }; + c.prototype.predraw = function(h) { + if (!this.is_generated_div_) { + return + } + h.dygraph.graphDiv.appendChild(this.legend_div_); + var g = h.dygraph.plotter_.area; + var f = h.dygraph.getOption("labelsDivWidth"); + this.legend_div_.style.left = g.x + g.w - f - 1 + "px"; + this.legend_div_.style.top = g.y + "px"; + this.legend_div_.style.width = f + "px" + }; + c.prototype.destroy = function() { + this.legend_div_ = null + }; + a = function(w, p, l, f) { + if (w.getOption("showLabelsOnHighlight") !== true) { + return "" + } + var r, C, u, s, m; + var z = w.getLabels(); + if (typeof(p) === "undefined") { + if (w.getOption("legend") != "always") { + return "" + } + C = w.getOption("labelsSeparateLines"); + r = ""; + for (u = 1; u < z.length; u++) { + var q = w.getPropertiesForSeries(z[u]); + if (!q.visible) { + continue + } + if (r !== "") { + r += (C ? "
" : " ") + } + m = w.getOption("strokePattern", z[u]); + s = d(m, q.color, f); + r += "" + s + " " + z[u] + "" + } + return r + } + var A = w.optionsViewForAxis_("x"); + var o = A("valueFormatter"); + r = o(p, A, z[0], w); + if (r !== "") { + r += ":" + } + var v = []; + var j = w.numAxes(); + for (u = 0; u < j; u++) { + v[u] = w.optionsViewForAxis_("y" + (u ? 1 + u : "")) + } + var k = w.getOption("labelsShowZeroValues"); + C = w.getOption("labelsSeparateLines"); + var B = w.getHighlightSeries(); + for (u = 0; u < l.length; u++) { + var t = l[u]; + if (t.yval === 0 && !k) { + continue + } + if (!Dygraph.isOK(t.canvasy)) { + continue + } + if (C) { + r += "
" + } + var q = w.getPropertiesForSeries(t.name); + var n = v[q.axis - 1]; + var y = n("valueFormatter"); + var e = y(t.yval, n, t.name, w); + var h = (t.name == B) ? " class='highlight'" : ""; + r += " " + t.name + ": " + e + "" + } + return r + }; + d = function(s, h, r) { + var e = (/MSIE/.test(navigator.userAgent) && !window.opera); + if (e) { + return "—" + } + if (!s || s.length <= 1) { + return '
' + } + var l, k, f, o; + var g = 0, + q = 0; + var p = []; + var n; + for (l = 0; l <= s.length; l++) { + g += s[l % s.length] + } + n = Math.floor(r / (g - s[0])); + if (n > 1) { + for (l = 0; l < s.length; l++) { + p[l] = s[l] / r + } + q = p.length + } else { + n = 1; + for (l = 0; l < s.length; l++) { + p[l] = s[l] / g + } + q = p.length + 1 + } + var m = ""; + for (k = 0; k < n; k++) { + for (l = 0; l < q; l += 2) { + f = p[l % p.length]; + if (l < s.length) { + o = p[(l + 1) % p.length] + } else { + o = 0 + } + m += '
' + } + } + return m + }; + return c +})(); +Dygraph.Plugins.RangeSelector = (function() { + var a = function() { + this.isIE_ = /MSIE/.test(navigator.userAgent) && !window.opera; + this.hasTouchInterface_ = typeof(TouchEvent) != "undefined"; + this.isMobileDevice_ = /mobile|android/gi.test(navigator.appVersion); + this.interfaceCreated_ = false + }; + a.prototype.toString = function() { + return "RangeSelector Plugin" + }; + a.prototype.activate = function(b) { + this.dygraph_ = b; + this.isUsingExcanvas_ = b.isUsingExcanvas_; + if (this.getOption_("showRangeSelector")) { + this.createInterface_() + } + return { + layout: this.reserveSpace_, + predraw: this.renderStaticLayer_, + didDrawChart: this.renderInteractiveLayer_ + } + }; + a.prototype.destroy = function() { + this.bgcanvas_ = null; + this.fgcanvas_ = null; + this.leftZoomHandle_ = null; + this.rightZoomHandle_ = null; + this.iePanOverlay_ = null + }; + a.prototype.getOption_ = function(b) { + return this.dygraph_.getOption(b) + }; + a.prototype.setDefaultOption_ = function(b, c) { + return this.dygraph_.attrs_[b] = c + }; + a.prototype.createInterface_ = function() { + this.createCanvases_(); + if (this.isUsingExcanvas_) { + this.createIEPanOverlay_() + } + this.createZoomHandles_(); + this.initInteraction_(); + if (this.getOption_("animatedZooms")) { + this.dygraph_.warn("Animated zooms and range selector are not compatible; disabling animatedZooms."); + this.dygraph_.updateOptions({ + animatedZooms: false + }, true) + } + this.interfaceCreated_ = true; + this.addToGraph_() + }; + a.prototype.addToGraph_ = function() { + var b = this.graphDiv_ = this.dygraph_.graphDiv; + b.appendChild(this.bgcanvas_); + b.appendChild(this.fgcanvas_); + b.appendChild(this.leftZoomHandle_); + b.appendChild(this.rightZoomHandle_) + }; + a.prototype.removeFromGraph_ = function() { + var b = this.graphDiv_; + b.removeChild(this.bgcanvas_); + b.removeChild(this.fgcanvas_); + b.removeChild(this.leftZoomHandle_); + b.removeChild(this.rightZoomHandle_); + this.graphDiv_ = null + }; + a.prototype.reserveSpace_ = function(b) { + if (this.getOption_("showRangeSelector")) { + b.reserveSpaceBottom(this.getOption_("rangeSelectorHeight") + 4) + } + }; + a.prototype.renderStaticLayer_ = function() { + if (!this.updateVisibility_()) { + return + } + this.resize_(); + this.drawStaticLayer_() + }; + a.prototype.renderInteractiveLayer_ = function() { + if (!this.updateVisibility_() || this.isChangingRange_) { + return + } + this.placeZoomHandles_(); + this.drawInteractiveLayer_() + }; + a.prototype.updateVisibility_ = function() { + var b = this.getOption_("showRangeSelector"); + if (b) { + if (!this.interfaceCreated_) { + this.createInterface_() + } else { + if (!this.graphDiv_ || !this.graphDiv_.parentNode) { + this.addToGraph_() + } + } + } else { + if (this.graphDiv_) { + this.removeFromGraph_(); + var c = this.dygraph_; + setTimeout(function() { + c.width_ = 0; + c.resize() + }, 1) + } + } + return b + }; + a.prototype.resize_ = function() { + function d(e, f) { + e.style.top = f.y + "px"; + e.style.left = f.x + "px"; + e.width = f.w; + e.height = f.h; + e.style.width = e.width + "px"; + e.style.height = e.height + "px" + } + var c = this.dygraph_.layout_.getPlotArea(); + var b = 0; + if (this.getOption_("drawXAxis")) { + b = this.getOption_("xAxisHeight") || (this.getOption_("axisLabelFontSize") + 2 * this.getOption_("axisTickSize")) + } + this.canvasRect_ = { + x: c.x, + y: c.y + c.h + b + 4, + w: c.w, + h: this.getOption_("rangeSelectorHeight") + }; + d(this.bgcanvas_, this.canvasRect_); + d(this.fgcanvas_, this.canvasRect_) + }; + a.prototype.createCanvases_ = function() { + this.bgcanvas_ = Dygraph.createCanvas(); + this.bgcanvas_.className = "dygraph-rangesel-bgcanvas"; + this.bgcanvas_.style.position = "absolute"; + this.bgcanvas_.style.zIndex = 9; + this.bgcanvas_ctx_ = Dygraph.getContext(this.bgcanvas_); + this.fgcanvas_ = Dygraph.createCanvas(); + this.fgcanvas_.className = "dygraph-rangesel-fgcanvas"; + this.fgcanvas_.style.position = "absolute"; + this.fgcanvas_.style.zIndex = 9; + this.fgcanvas_.style.cursor = "default"; + this.fgcanvas_ctx_ = Dygraph.getContext(this.fgcanvas_) + }; + a.prototype.createIEPanOverlay_ = function() { + this.iePanOverlay_ = document.createElement("div"); + this.iePanOverlay_.style.position = "absolute"; + this.iePanOverlay_.style.backgroundColor = "white"; + this.iePanOverlay_.style.filter = "alpha(opacity=0)"; + this.iePanOverlay_.style.display = "none"; + this.iePanOverlay_.style.cursor = "move"; + this.fgcanvas_.appendChild(this.iePanOverlay_) + }; + a.prototype.createZoomHandles_ = function() { + var b = new Image(); + b.className = "dygraph-rangesel-zoomhandle"; + b.style.position = "absolute"; + b.style.zIndex = 10; + b.style.visibility = "hidden"; + b.style.cursor = "col-resize"; + if (/MSIE 7/.test(navigator.userAgent)) { + b.width = 7; + b.height = 14; + b.style.backgroundColor = "white"; + b.style.border = "1px solid #333333" + } else { + b.width = 9; + b.height = 16; + b.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAAXNSR0IArs4c6QAAAAZiS0dEANAAzwDP4Z7KegAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9sHGw0cMqdt1UwAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAaElEQVQoz+3SsRFAQBCF4Z9WJM8KCDVwownl6YXsTmCUsyKGkZzcl7zkz3YLkypgAnreFmDEpHkIwVOMfpdi9CEEN2nGpFdwD03yEqDtOgCaun7sqSTDH32I1pQA2Pb9sZecAxc5r3IAb21d6878xsAAAAAASUVORK5CYII=" + } + if (this.isMobileDevice_) { + b.width *= 2; + b.height *= 2 + } + this.leftZoomHandle_ = b; + this.rightZoomHandle_ = b.cloneNode(false) + }; + a.prototype.initInteraction_ = function() { + var o = this; + var i = this.isIE_ ? document : window; + var u = 0; + var v = null; + var s = false; + var d = false; + var g = !this.isMobileDevice_ && !this.isUsingExcanvas_; + var k = new Dygraph.IFrameTarp(); + var p, f, r, j, w, h, x, t, q, c, l; + var e, n, m; + p = function(C) { + var B = o.dygraph_.xAxisExtremes(); + var z = (B[1] - B[0]) / o.canvasRect_.w; + var A = B[0] + (C.leftHandlePos - o.canvasRect_.x) * z; + var y = B[0] + (C.rightHandlePos - o.canvasRect_.x) * z; + return [A, y] + }; + f = function(y) { + Dygraph.cancelEvent(y); + s = true; + u = y.clientX; + v = y.target ? y.target : y.srcElement; + if (y.type === "mousedown" || y.type === "dragstart") { + Dygraph.addEvent(i, "mousemove", r); + Dygraph.addEvent(i, "mouseup", j) + } + o.fgcanvas_.style.cursor = "col-resize"; + k.cover(); + return true + }; + r = function(C) { + if (!s) { + return false + } + Dygraph.cancelEvent(C); + var z = C.clientX - u; + if (Math.abs(z) < 4) { + return true + } + u = C.clientX; + var B = o.getZoomHandleStatus_(); + var y; + if (v == o.leftZoomHandle_) { + y = B.leftHandlePos + z; + y = Math.min(y, B.rightHandlePos - v.width - 3); + y = Math.max(y, o.canvasRect_.x) + } else { + y = B.rightHandlePos + z; + y = Math.min(y, o.canvasRect_.x + o.canvasRect_.w); + y = Math.max(y, B.leftHandlePos + v.width + 3) + } + var A = v.width / 2; + v.style.left = (y - A) + "px"; + o.drawInteractiveLayer_(); + if (g) { + w() + } + return true + }; + j = function(y) { + if (!s) { + return false + } + s = false; + k.uncover(); + Dygraph.removeEvent(i, "mousemove", r); + Dygraph.removeEvent(i, "mouseup", j); + o.fgcanvas_.style.cursor = "default"; + if (!g) { + w() + } + return true + }; + w = function() { + try { + var z = o.getZoomHandleStatus_(); + o.isChangingRange_ = true; + if (!z.isZoomed) { + o.dygraph_.resetZoom() + } else { + var y = p(z); + o.dygraph_.doZoomXDates_(y[0], y[1]) + } + } finally { + o.isChangingRange_ = false + } + }; + h = function(A) { + if (o.isUsingExcanvas_) { + return A.srcElement == o.iePanOverlay_ + } else { + var z = o.leftZoomHandle_.getBoundingClientRect(); + var y = z.left + z.width / 2; + z = o.rightZoomHandle_.getBoundingClientRect(); + var B = z.left + z.width / 2; + return (A.clientX > y && A.clientX < B) + } + }; + x = function(y) { + if (!d && h(y) && o.getZoomHandleStatus_().isZoomed) { + Dygraph.cancelEvent(y); + d = true; + u = y.clientX; + if (y.type === "mousedown") { + Dygraph.addEvent(i, "mousemove", t); + Dygraph.addEvent(i, "mouseup", q) + } + return true + } + return false + }; + t = function(C) { + if (!d) { + return false + } + Dygraph.cancelEvent(C); + var z = C.clientX - u; + if (Math.abs(z) < 4) { + return true + } + u = C.clientX; + var B = o.getZoomHandleStatus_(); + var E = B.leftHandlePos; + var y = B.rightHandlePos; + var D = y - E; + if (E + z <= o.canvasRect_.x) { + E = o.canvasRect_.x; + y = E + D + } else { + if (y + z >= o.canvasRect_.x + o.canvasRect_.w) { + y = o.canvasRect_.x + o.canvasRect_.w; + E = y - D + } else { + E += z; + y += z + } + } + var A = o.leftZoomHandle_.width / 2; + o.leftZoomHandle_.style.left = (E - A) + "px"; + o.rightZoomHandle_.style.left = (y - A) + "px"; + o.drawInteractiveLayer_(); + if (g) { + c() + } + return true + }; + q = function(y) { + if (!d) { + return false + } + d = false; + Dygraph.removeEvent(i, "mousemove", t); + Dygraph.removeEvent(i, "mouseup", q); + if (!g) { + c() + } + return true + }; + c = function() { + try { + o.isChangingRange_ = true; + o.dygraph_.dateWindow_ = p(o.getZoomHandleStatus_()); + o.dygraph_.drawGraph_(false) + } finally { + o.isChangingRange_ = false + } + }; + l = function(y) { + if (s || d) { + return + } + var z = h(y) ? "move" : "default"; + if (z != o.fgcanvas_.style.cursor) { + o.fgcanvas_.style.cursor = z + } + }; + e = function(y) { + if (y.type == "touchstart" && y.targetTouches.length == 1) { + if (f(y.targetTouches[0])) { + Dygraph.cancelEvent(y) + } + } else { + if (y.type == "touchmove" && y.targetTouches.length == 1) { + if (r(y.targetTouches[0])) { + Dygraph.cancelEvent(y) + } + } else { + j(y) + } + } + }; + n = function(y) { + if (y.type == "touchstart" && y.targetTouches.length == 1) { + if (x(y.targetTouches[0])) { + Dygraph.cancelEvent(y) + } + } else { + if (y.type == "touchmove" && y.targetTouches.length == 1) { + if (t(y.targetTouches[0])) { + Dygraph.cancelEvent(y) + } + } else { + q(y) + } + } + }; + m = function(B, A) { + var z = ["touchstart", "touchend", "touchmove", "touchcancel"]; + for (var y = 0; y < z.length; y++) { + o.dygraph_.addAndTrackEvent(B, z[y], A) + } + }; + this.setDefaultOption_("interactionModel", Dygraph.Interaction.dragIsPanInteractionModel); + this.setDefaultOption_("panEdgeFraction", 0.0001); + var b = window.opera ? "mousedown" : "dragstart"; + this.dygraph_.addAndTrackEvent(this.leftZoomHandle_, b, f); + this.dygraph_.addAndTrackEvent(this.rightZoomHandle_, b, f); + if (this.isUsingExcanvas_) { + this.dygraph_.addAndTrackEvent(this.iePanOverlay_, "mousedown", x) + } else { + this.dygraph_.addAndTrackEvent(this.fgcanvas_, "mousedown", x); + this.dygraph_.addAndTrackEvent(this.fgcanvas_, "mousemove", l) + } + if (this.hasTouchInterface_) { + m(this.leftZoomHandle_, e); + m(this.rightZoomHandle_, e); + m(this.fgcanvas_, n) + } + }; + a.prototype.drawStaticLayer_ = function() { + var b = this.bgcanvas_ctx_; + b.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h); + try { + this.drawMiniPlot_() + } catch (c) { + Dygraph.warn(c) + } + var d = 0.5; + this.bgcanvas_ctx_.lineWidth = 1; + b.strokeStyle = "gray"; + b.beginPath(); + b.moveTo(d, d); + b.lineTo(d, this.canvasRect_.h - d); + b.lineTo(this.canvasRect_.w - d, this.canvasRect_.h - d); + b.lineTo(this.canvasRect_.w - d, d); + b.stroke() + }; + a.prototype.drawMiniPlot_ = function() { + var f = this.getOption_("rangeSelectorPlotFillColor"); + var r = this.getOption_("rangeSelectorPlotStrokeColor"); + if (!f && !r) { + return + } + var j = this.getOption_("stepPlot"); + var v = this.computeCombinedSeriesAndLimits_(); + var q = v.yMax - v.yMin; + var p = this.bgcanvas_ctx_; + var n = 0.5; + var e = this.dygraph_.xAxisExtremes(); + var o = Math.max(e[1] - e[0], 1e-30); + var g = (this.canvasRect_.w - n) / o; + var u = (this.canvasRect_.h - n) / q; + var t = this.canvasRect_.w - n; + var b = this.canvasRect_.h - n; + var d = null, + c = null; + p.beginPath(); + p.moveTo(n, b); + for (var s = 0; s < v.data.length; s++) { + var h = v.data[s]; + var l = ((h[0] !== null) ? ((h[0] - e[0]) * g) : NaN); + var k = ((h[1] !== null) ? (b - (h[1] - v.yMin) * u) : NaN); + if (isFinite(l) && isFinite(k)) { + if (d === null) { + p.lineTo(l, b) + } else { + if (j) { + p.lineTo(l, c) + } + } + p.lineTo(l, k); + d = l; + c = k + } else { + if (d !== null) { + if (j) { + p.lineTo(l, c); + p.lineTo(l, b) + } else { + p.lineTo(d, b) + } + } + d = c = null + } + } + p.lineTo(t, b); + p.closePath(); + if (f) { + var m = this.bgcanvas_ctx_.createLinearGradient(0, 0, 0, b); + m.addColorStop(0, "white"); + m.addColorStop(1, f); + this.bgcanvas_ctx_.fillStyle = m; + p.fill() + } + if (r) { + this.bgcanvas_ctx_.strokeStyle = r; + this.bgcanvas_ctx_.lineWidth = 1.5; + p.stroke() + } + }; + a.prototype.computeCombinedSeriesAndLimits_ = function() { + var v = this.dygraph_.rawData_; + var u = this.getOption_("logscale"); + var q = []; + var d; + var h; + var m; + var t, s, r; + var e, g; + for (t = 0; t < v.length; t++) { + if (v[t].length > 1 && v[t][1] !== null) { + m = typeof v[t][1] != "number"; + if (m) { + d = []; + h = []; + for (r = 0; r < v[t][1].length; r++) { + d.push(0); + h.push(0) + } + } + break + } + } + for (t = 0; t < v.length; t++) { + var l = v[t]; + e = l[0]; + if (m) { + for (r = 0; r < d.length; r++) { + d[r] = h[r] = 0 + } + } else { + d = h = 0 + } + for (s = 1; s < l.length; s++) { + if (this.dygraph_.visibility()[s - 1]) { + var n; + if (m) { + for (r = 0; r < d.length; r++) { + n = l[s][r]; + if (n === null || isNaN(n)) { + continue + } + d[r] += n; + h[r]++ + } + } else { + n = l[s]; + if (n === null || isNaN(n)) { + continue + } + d += n; + h++ + } + } + } + if (m) { + for (r = 0; r < d.length; r++) { + d[r] /= h[r] + } + g = d.slice(0) + } else { + g = d / h + } + q.push([e, g]) + } + q = this.dygraph_.rollingAverage(q, this.dygraph_.rollPeriod_); + if (typeof q[0][1] != "number") { + for (t = 0; t < q.length; t++) { + g = q[t][1]; + q[t][1] = g[0] + } + } + var b = Number.MAX_VALUE; + var c = -Number.MAX_VALUE; + for (t = 0; t < q.length; t++) { + g = q[t][1]; + if (g !== null && isFinite(g) && (!u || g > 0)) { + b = Math.min(b, g); + c = Math.max(c, g) + } + } + var o = 0.25; + if (u) { + c = Dygraph.log10(c); + c += c * o; + b = Dygraph.log10(b); + for (t = 0; t < q.length; t++) { + q[t][1] = Dygraph.log10(q[t][1]) + } + } else { + var f; + var p = c - b; + if (p <= Number.MIN_VALUE) { + f = c * o + } else { + f = p * o + } + c += f; + b -= f + } + return { + data: q, + yMin: b, + yMax: c + } + }; + a.prototype.placeZoomHandles_ = function() { + var h = this.dygraph_.xAxisExtremes(); + var b = this.dygraph_.xAxisRange(); + var c = h[1] - h[0]; + var j = Math.max(0, (b[0] - h[0]) / c); + var f = Math.max(0, (h[1] - b[1]) / c); + var i = this.canvasRect_.x + this.canvasRect_.w * j; + var e = this.canvasRect_.x + this.canvasRect_.w * (1 - f); + var d = Math.max(this.canvasRect_.y, this.canvasRect_.y + (this.canvasRect_.h - this.leftZoomHandle_.height) / 2); + var g = this.leftZoomHandle_.width / 2; + this.leftZoomHandle_.style.left = (i - g) + "px"; + this.leftZoomHandle_.style.top = d + "px"; + this.rightZoomHandle_.style.left = (e - g) + "px"; + this.rightZoomHandle_.style.top = this.leftZoomHandle_.style.top; + this.leftZoomHandle_.style.visibility = "visible"; + this.rightZoomHandle_.style.visibility = "visible" + }; + a.prototype.drawInteractiveLayer_ = function() { + var c = this.fgcanvas_ctx_; + c.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h); + var f = 1; + var e = this.canvasRect_.w - f; + var b = this.canvasRect_.h - f; + var h = this.getZoomHandleStatus_(); + c.strokeStyle = "black"; + if (!h.isZoomed) { + c.beginPath(); + c.moveTo(f, f); + c.lineTo(f, b); + c.lineTo(e, b); + c.lineTo(e, f); + c.stroke(); + if (this.iePanOverlay_) { + this.iePanOverlay_.style.display = "none" + } + } else { + var g = Math.max(f, h.leftHandlePos - this.canvasRect_.x); + var d = Math.min(e, h.rightHandlePos - this.canvasRect_.x); + c.fillStyle = "rgba(240, 240, 240, 0.6)"; + c.fillRect(0, 0, g, this.canvasRect_.h); + c.fillRect(d, 0, this.canvasRect_.w - d, this.canvasRect_.h); + c.beginPath(); + c.moveTo(f, f); + c.lineTo(g, f); + c.lineTo(g, b); + c.lineTo(d, b); + c.lineTo(d, f); + c.lineTo(e, f); + c.stroke(); + if (this.isUsingExcanvas_) { + this.iePanOverlay_.style.width = (d - g) + "px"; + this.iePanOverlay_.style.left = g + "px"; + this.iePanOverlay_.style.height = b + "px"; + this.iePanOverlay_.style.display = "inline" + } + } + }; + a.prototype.getZoomHandleStatus_ = function() { + var c = this.leftZoomHandle_.width / 2; + var d = parseFloat(this.leftZoomHandle_.style.left) + c; + var b = parseFloat(this.rightZoomHandle_.style.left) + c; + return { + leftHandlePos: d, + rightHandlePos: b, + isZoomed: (d - 1 > this.canvasRect_.x || b + 1 < this.canvasRect_.x + this.canvasRect_.w) + } + }; + return a +})(); +Dygraph.PLUGINS.push(Dygraph.Plugins.Legend, Dygraph.Plugins.Axes, Dygraph.Plugins.RangeSelector, Dygraph.Plugins.ChartLabels, Dygraph.Plugins.Annotations, Dygraph.Plugins.Grid); diff --git a/.github/workflows/check_json.yml b/.github/workflows/check_json.yml new file mode 100644 index 0000000..e9f0463 --- /dev/null +++ b/.github/workflows/check_json.yml @@ -0,0 +1,25 @@ +name: Check JSON Validity + +on: + pull_request: + push: + branches: + - master + +jobs: + validate-json: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Python3 + uses: actions/setup-python@v5 + + - name: Validate JSON files + run: | + set -e + for file in $(find configs/ -type f -name "*.json"); do + echo "Checking $file" + python3 -m json.tool "$file" > /dev/null + done diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..852e4b9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,96 @@ +name: Continuous Integration + +on: + pull_request: + push: + branches: + - master + tags: ['*'] + +jobs: + test: + name: GitHub Actions Test + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + omc-version: [stable, nightly] + python-version: ['3.10'] + include: + - os: windows-latest + omc-version: stable + msysEnvironment: --msysEnvironment=ucrt64 + fail-fast: false + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup OpenModelica + uses: OpenModelica/setup-openmodelica@v1 + with: + version: ${{ matrix.omc-version }} + packages: | + 'omc' + libraries: | + 'Modelica 4.0.0' + omc-diff: true + + - name: Check if .CI/installLibraries.mos works + shell: bash + run: | + if [ "$RUNNER_OS" == "Linux" ]; then + omc .CI/installLibraries.mos + fi + + - name: Setup Python3 + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' # caching pip dependencies + + - name: Install Python dependencies + shell: bash + run: | + pip install -r requirements.txt + + - name: Run library test + shell: bash + env: + MY_SANITY_CHECK_DIRECTORY: sanityCheck + run: | + python test.py --branch="${{ matrix.omc-version }}" --noclean --verbose ${{ matrix.msysEnvironment }} configs/sanityCheck.json + + - name: Generate HTML results + shell: bash + run: python report.py --branches="${{ matrix.omc-version }}" configs/sanityCheck.json + + - name: Zip HTML results + shell: bash + run: | + python .github/scripts/archiveResults.py "MyLibrary" "1.0.0" "${{ matrix.omc-version }}" "html/" + + - name: Verify that overview.html contains simulation and verification + shell: bash + run: | + if ! grep -q '${{ matrix.omc-version }}222222222' overview.html; then + echo "Failed to find 2 simulating and 2 verifying models in overwiew.html:" + cat overview.html + exit 1 + fi + + - name: Archive sqlite3.db + uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ matrix.os }}-${{ matrix.omc-version }}-sqlite3.db + path: | + sqlite3.db + + - name: Archive HTML + uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ matrix.os }}-${{ matrix.omc-version }}-MyLibrary.html + path: html/ diff --git a/.gitignore b/.gitignore index b1a0309..409402b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,16 @@ +__pycache__ +/.CI/wasm-jit/requirements.txt +!HelloWorld.mo +!HelloWorld.mos +/.venv/ +/.vscode/ +/*.files +/*.html /*.json /converted-libraries -__pycache__ +/MyLibrary*/ +/ReferenceFiles/ +files/ +HelloWorld* +history/ +sqlite3.db diff --git a/OSMC-License.txt b/OSMC-License.txt new file mode 100644 index 0000000..10d0e2e --- /dev/null +++ b/OSMC-License.txt @@ -0,0 +1,271 @@ +--- Start of Definition of OSMC Public License --- + +/* + * This file is part of OpenModelica. + * + * Copyright (c) 1998-CurrentYear, Open Source Modelica Consortium (OSMC), + * c/o Linköpings universitet, Department of Computer and Information Science, + * SE-58183 Linköping, Sweden. + * + * All rights reserved. + * + * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF GPL VERSION 3 LICENSE OR + * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.2. + * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES + * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GPL VERSION 3, + * ACCORDING TO RECIPIENTS CHOICE. + * + * The OpenModelica software and the Open Source Modelica + * Consortium (OSMC) Public License (OSMC-PL) are obtained + * from OSMC, either from the above address, + * from the URLs: http://www.ida.liu.se/projects/OpenModelica or + * http://www.openmodelica.org, and in the OpenModelica distribution. + * GNU version 3 is obtained from: http://www.gnu.org/copyleft/gpl.html. + * + * This program is distributed WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH + * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. + * + * See the full OSMC Public License conditions for more details. + * + */ + +--- End of OSMC Public License Header --- + +The OSMC-PL is a public license for OpenModelica with three modes/alternatives +(GPL, OSMC-Internal-EPL, OSMC-External-EPL) for use and redistribution, +in source and/or binary/object-code form: + +* GPL. Any party (member or non-member of OSMC) may use and redistribute + OpenModelica under GPL version 3. + +* Level 1 members of OSMC may also use and redistribute OpenModelica under + OSMC-Internal-EPL conditions. + +* Level 2 members of OSMC may also use and redistribute OpenModelica under + OSMC-Internal-EPL or OSMC-External-EPL conditions. + +Definitions of OSMC Public license modes: + +* GPL = GPL version 3. + +* OSMC-Internal-EPL = These OSMC Public license conditions together with + Internally restricted EPL, i.e., EPL version 1.0 with the Additional + Condition that use and redistribution by an OSMC member is only allowed + within the OSMC member's own organization (i.e., its own legal entity), + or for an OSMC member paying a membership fee corresponding to the size + of the organization including all its affiliates, use and redistribution + is allowed within/between its affiliates. + +* OSMC-External-EPL = These OSMC Public license conditions together with + Externally restricted EPL, i.e., EPL version 1.0 with the Additional + Condition that use and redistribution by an OSMC member, or by a Licensed + Third Party Distributor having a redistribution agreement with that member, + to parties external to the OSMC member’s own organization (i.e., its own + legal entity) is only allowed in binary/object-code form, except the case of + redistribution to other OSMC members to which source is also allowed to be + distributed. + +[This has the consequence that an external party who wishes to use +OpenModelica in source form together with its own proprietary software in all +cases must be a member of OSMC]. + +In all cases of usage and redistribution by recipients, the following +conditions also apply: + +a) Redistributions of source code must retain the above copyright notice, + all definitions, and conditions. It is sufficient if the OSMC-PL Header is + present in each source file, if the full OSMC-PL is available in a prominent + and easily located place in the redistribution. + +b) Redistributions in binary/object-code form must reproduce the above + copyright notice, all definitions, and conditions. It is sufficient if the + OSMC-PL Header and the location in the redistribution of the full OSMC-PL + are present in the documentation and/or other materials provided with the + redistribution, if the full OSMC-PL is available in a prominent and easily + located place in the redistribution. + +c) A recipient must clearly indicate its chosen usage mode of OSMC-PL, + in accompanying documentation and in a text file OSMC-USAGE-MODE.txt, + provided with the distribution. + +d) Contributor(s) making a Contribution to OpenModelica thereby also makes a + Transfer of Contribution Copyright. In return, upon the effective date of + the transfer, OSMC grants the Contributor(s) a Contribution License of the + Contribution. OSMC has the right to accept or refuse Contributions. + +Definitions: + +"Subsidiary license conditions" means: + +The additional license conditions depending on the by the recipient chosen +mode of OSMC-PL, defined by GPL version 3.0 for GPL, and by EPL for +OSMC-Internal-EPL and OSMC-External-EPL. + +"OSMC-PL" means: + +Open Source Modelica Consortium Public License version 1.2, i.e., the license +defined here (the text between +"--- Start of Definition of OSMC Public License ---" and +"--- End of Definition of OSMC Public License ---", or later versions thereof. + +"OSMC-PL Header" means: + +Open Source Modelica Consortium Public License Header version 1.2, i.e., the +text between "--- Start of Definition of OSMC Public License ---" and +"--- End of OSMC Public License Header ---, or later versions thereof. + +"Contribution" means: + +a) in the case of the initial Contributor, the initial code and documentation + distributed under OSMC-PL, and + +b) in the case of each subsequent Contributor: + i) changes to OpenModelica, and + ii) additions to OpenModelica; + +where such changes and/or additions to OpenModelica originate from and are +distributed by that particular Contributor. A Contribution 'originates' from +a Contributor if it was added to OpenModelica by such Contributor itself or +anyone acting on such Contributor's behalf. + +For Contributors licensing OpenModelica under OSMC-Internal-EPL or +OSMC-External-EPL conditions, the following conditions also hold: + +Contributions do not include additions to the distributed Program which: (i) +are separate modules of software distributed in conjunction with OpenModelica +under their own license agreement, (ii) are separate modules which are not +derivative works of OpenModelica, and (iii) are separate modules of software +distributed in conjunction with OpenModelica under their own license agreement +where these separate modules are merged with (weaved together with) modules of +OpenModelica to form new modules that are distributed as object code or source +code under their own license agreement, as allowed under the Additional +Condition of internal distribution according to OSMC-Internal-EPL and/or +Additional Condition for external distribution according to OSMC-External-EPL. + +"Transfer of Contribution Copyright" means that the Contributors of a +Contribution transfer the ownership and the copyright of the Contribution to +Open Source Modelica Consortium, the OpenModelica Copyright owner, for +inclusion in OpenModelica. The transfer takes place upon the effective date +when the Contribution is made available on the OSMC web site under OSMC-PL, by +such Contributors themselves or anyone acting on such Contributors' behalf. +The transfer is free of charge. If the Contributors or OSMC so wish, +an optional Copyright transfer agreement can be signed between OSMC and the +Contributors, as specified in an Appendix of the OSMC Bylaws. + +"Contribution License" means a license from OSMC to the Contributors of the +Contribution, effective on the date of the Transfer of Contribution Copyright, +where OSMC grants the Contributors a non-exclusive, world-wide, transferable, +free of charge, perpetual license, including sublicensing rights, to use, +have used, modify, have modified, reproduce and or have reproduced the +contributed material, for business and other purposes, including but not +limited to evaluation, development, testing, integration and merging with +other software and distribution. The warranty and liability disclaimers of +OSMC-PL apply to this license. + +"Contributor" means any person or entity that distributes (part of) +OpenModelica. + +"The Program" means the Contributions distributed in accordance with OSMC-PL. + +"OpenModelica" means the Contributions distributed in accordance with OSMC-PL. + +"Recipient" means anyone who receives OpenModelica under OSMC-PL, +including all Contributors. + +"Licensed Third Party Distributor" means a reseller/distributor having signed +a redistribution/resale agreement in accordance with OSMC-PL and OSMC Bylaws, +with an OSMC Level 2 organizational member which is not an Affiliate of the +reseller/distributor, for distributing a product containing part(s) of +OpenModelica. The Licensed Third Party Distributor shall only be allowed +further redistribution to other resellers if the Level 2 member is granting +such a right to it in the redistribution/resale agreement between the +Level 2 member and the Licensed Third Party Distributor. + +"Affiliate" shall mean any legal entity, directly or indirectly, through one +or more intermediaries, controlling or controlled by or under common control +with any other legal entity, as the case may be. For purposes of this +definition, the term "control" (including the terms "controlling," +"controlled by" and "under common control with") means the possession, +direct or indirect, of the power to direct or cause the direction of the +management and policies of a legal entity, whether through the ownership of +voting securities, by contract or otherwise. + +NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THE BY RECIPIENT SELECTED SUBSIDIARY +LICENSE CONDITIONS OF OSMC-PL, OPENMODELICA IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing OPENMODELICA and assumes all risks +associated with its exercise of rights under OSMC-PL , including but not +limited to the risks and costs of program errors, compliance with applicable +laws, damage to or loss of data, programs or equipment, and unavailability +or interruption of operations. + +DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THE BY RECIPIENT SELECTED SUBSIDIARY +LICENSE CONDITIONS OF OSMC-PL, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION +LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF OPENMODELICA OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +A Contributor licensing OpenModelica under OSMC-Internal-EPL or +OSMC-External-EPL may choose to distribute (parts of) OpenModelica in object +code form under its own license agreement, provided that: + +a) it complies with the terms and conditions of OSMC-PL; or for the case of +redistribution of OpenModelica together with proprietary code it is a dual +license where the OpenModelica parts are distributed under OSMC-PL compatible +conditions and the proprietary code is distributed under proprietary license +conditions; and + +b) its license agreement: + i) effectively disclaims on behalf of all Contributors all warranties and +conditions, express and implied, including warranties or conditions of title +and non-infringement, and implied warranties or conditions of merchantability +and fitness for a particular purpose; + ii) effectively excludes on behalf of all Contributors all liability for +damages, including direct, indirect, special, incidental and consequential +damages, such as lost profits; + iii) states that any provisions which differ from OSMC-PL are offered by that +Contributor alone and not by any other party; and + iv) states from where the source code for OpenModelica is available, and +informs licensees how to obtain it in a reasonable manner on or through a +medium customarily used for software exchange. + +When OPENMODELICA is made available in source code form: + + a) it must be made available under OSMC-PL; and + + b) a copy of OSMC-PL must be included with each copy of OPENMODELICA. + + c) a copy of the subsidiary license associated with the selected mode of +OSMC-PL must be included with each copy of OPENMODELICA. + +Contributors may not remove or alter any copyright notices contained within +OPENMODELICA. + +If there is a conflict between OSMC-PL and the subsidiary license conditions, +OSMC-PL has priority. + +This Agreement is governed by the laws of Sweden. The place of jurisdiction +for all disagreements related to this Agreement, is Linköping, Sweden. + +The EPL 1.0 license definition has been obtained from: +http://www.eclipse.org/legal/epl-v10.html. It is also reproduced in Appendix B +of the OSMC Bylaws, and in the OpenModelica distribution. + +The GPL Version 3 license definition has been obtained from +http://www.gnu.org/copyleft/gpl.html. It is also reproduced in Appendix C +of the OSMC Bylaws, and in the OpenModelica distribution. + +--- End of Definition of OSMC Public License --- \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7bc562f --- /dev/null +++ b/README.md @@ -0,0 +1,370 @@ +# OpenModelica Library Testing + +[![Continuous Integration](https://github.com/OpenModelica/OpenModelicaLibraryTesting/actions/workflows/test.yml/badge.svg)](https://github.com/OpenModelica/OpenModelicaLibraryTesting/actions/workflows/test.yml) +[![License: OSMC-PL](https://img.shields.io/badge/license-OSMC--PL-lightgrey.svg)](OSMC-License.txt) + +This repository provides scripts and documentation to run the nightly Modelica +library tests for OpenModelica. + +## OpenModelica nightly testsuite + +Some of the open-source Modelica libraries managed by the +[Open Modelica Package Manager](https://github.com/OpenModelica/OMPackageManager) +are tested on a daily basis on the OSMC servers. + +[Test results reports](testresults.md#open-source-modelica-library-testing-using-openmodelica) +are publicly available. + +The configuration file for the regular library nightly testsuite is +[conf.json](configs/conf.json). Additional old and non-standard libraries are +listed in [conf-old.json](configs/conf-old.json) and +[conf-nonstandard.json](configs/conf-nonstandard.json), note that failures in +those libraries may be due to the fact that they are not fully complying with +the Modelica standard, rather than to OpenModelica issues. The setup of the +configuration files is discussed in [conf-howto.md](conf-howto.md). + +Test results reports are collected in the +[https://libraries.openmodelica.org/branches/](libraries.openmodelica.org/branches) +directory. The +[overview.html](https://libraries.openmodelica.org/branches/overview.html) +report gives the results of the regular testsuite with the default C runtime and +solvers. Other reports contain the results using the C++ runtime, FMI, daeMode, +and the old frontend. Combined reports also include results from the old and +nonstandard libraries. The +[https://libraries.openmodelica.org/branches/history/](libraries.openmodelica.org/branches/history) +directory contains regression reports and plots using different versions +(including master) and simulation runtime configurations (C++, daeMode, FMI, old +frontend) of OpenModelica. + +If you want to include your open-source library in the testsuite, please open a +pull request on [conf.json](configs/conf.json), or open an issue on the +OpenModelica +[issue tracker](https://github.com/OpenModelica/OpenModelica/issues/new/choose) +and ask us to do it for you. + +## Running the library testing infrastructure on your own server + +The scripts from this repository can be used to run regression tests for public, +private, and commercial Modelica libraries to keep track of coverage with +different OpenModelica versions, according to the conditions of the +[OSMC-PL license](OSMC-License.txt). + +### Dependencies + +- [OpenModelica](https://openmodelica.org) +- [Python](https://www.python.org/) +- (Optional) Reference simulation result files + +### Set-Up + +- Install or build OpenModelica + - [Install instructions](https://openmodelica.org/download/download-linux) + - [Build instructions](https://github.com/OpenModelica/OpenModelica#readme) + - Make sure `omc` is in your `PATH` +- Install Python requirements + + ```bash + pip install -r requirements.txt + ``` + +- OMC will search for libraries in the location provided with test.py argument + `--libraries`. The default value is `/home/username/.openmodelica/libraries/` + (Linux) or `%APPDATA%/.openmodelica/libraries` (Windows). + - Install your libraries into the location specified with `--libraries` + or use `loadFile` command inside `loadFileCommands` in the config JSON: + + ```yml + "loadFileCommands": [ + "loadFile(\"/path/to/package.mo\")" + ] + ``` + +- Create configs/myConf.json to specify what libraries to test. + + ```json + [ + { + "library":"MyModelicaLibrary", + "libraryVersion":"main", + "libraryVersionExactMatch":true, // to be sure that the exact version is loaded, not the latest compatible + "libraryVersionLatestInPackageManager":true, // load the latest from the package manager + "referenceFileExtension":"mat", + "referenceFileNameDelimiter":"/", + "referenceFileNameExtraName":"$ClassName", + "referenceFiles": "/path/to/some/SomeDirectory", // specifies a directory with the files + "referenceFiles": "$ENV_VAR/SomeDirectory", // specifies a directory with the files via an env var + "referenceFiles":{ // specified as an URL, directory destination, git branch and git directory + "giturl":"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/myName/MyModelicaLibrary-ref", + "destination":"ReferenceFiles/MyModelicaLibrary", + "git-ref": "main", + "git-directory": "ReferenceFiles" + }, + "defaultTolerance": 1e-6, // tolerance for tests if not specified by the model, defaults to 1e-6 + "defaultNumberOfIntervals": 2500, // number of intervals for tests if not specified by the model, defaults to 2500 + "ulimitOmc":800, // specify a max timeout for a model build + "ulimitExe":300, // specify a max timeout for a model simulation + "ulimitMemory":62000000, // specify a max for the virtual memory of the running process when building a model + "procOMC":0, // [if procOMC = 0 use max procs, use procOMC = 1 if not defined, else use the given value] how many CPU cores should be used to run omc (load Modelica libraries in parallel and generate the C code in parallel) + "procCCompile":0, // [if procCCompile = 0 use max procs, use procCCompile = 1 if not defined, else use the given value] how many CPU cores should be used to compile the generated code + "optlevel":"-Os -march=native" // what optimizations should be used by the C compiler + } + ] + ``` + + You can add extra compiler settings + + ```json + "extraCustomCommands":["setCommandLineOptions(\"--std=3.2\");"] + ``` + + and extra simulation flags + + ```json + "extraSimFlags": "-s=ida -nls=kinsol" + ``` + + Check [configs/conf.json](./configs/conf.json) for more. +- If you used [.CI/installLibraries.mos](./.CI/installLibraries.mos) to test all + libraries you'll need to install reference results and set environment + variables, see [Reference Results](#reference-results). + + ```bash + export MSLREFERENCE="/path/to/ReferenceFiles/" + export REFERENCEFILES="/path/to/OpenModelica/testsuite/ReferenceFiles" + export PNLIBREFS="/path/to/ReferenceFiles/PNlib/ReferenceFiles" + export THERMOFLUIDSTREAMREFS="/path/to/ReferenceFiles/ThermofluidStream-main-regression/ReferenceData" + export THERMOFLUIDSTREAMREFSOM="/path/to/ReferenceFiles/ThermofluidStream-OM-regression/ReferenceData" + ``` + +### Library Test + +Run the library test: + +```bash +./test.py --noclean configs/myConf.json +``` + +Use `configs/*.json` to specify what to test. +The test results are saved in `sqlite3.db`. + +Options: + +- `--branch=master`: Branch of OpenModelica +- `--fmi=False`: Test FMI +- `--output=''`: Result location +- `--libraries=~/.openmodelica/libraries/`: Location of Modelica libraries +- `--extraflags=''`: Extra compiler flags. +- `--extrasimflags=''`: Extra simulation flags. +- `--ompython_omhome=''`: Path to OpenModelica for OMPython (can be different to + the OM running the tests) +- `--noclean=False`: Clean (most) generated files. +- `--fmisimulator=''`: The FMI simulator to run the FMUs with, as `name=command` + or just the command, e.g. the path to the OMSimulator + executable or `'python3 -m fmpy'`. Repeat the option to + simulate every FMU with several tools without building it + more than once, see [Testing FMI with several + simulators](#testing-fmi-with-several-simulators) +- `--ulimitvmem=8388608`: Virtual memory limit (in kB) +- `--default=[]`: Add a default value for some configuration key, such as + `--default=ulimitExe=60`. The equals sign is mandatory +- `-j`,`--jobs`: Number of cores to use for testing, default is `0` (max cores), + use `1` to run serial (for large tests) and see `procOMC` and + `procCCompile` above for more insight into individual test + parallelization. + +### Testing FMI with several simulators + +Building an FMU costs far more than simulating it. Measured on the twelve +models of ExternData: 178 seconds building the FMUs, 0.7 simulating them with +OMSimulator and 2.6 with FMPy. Testing the same FMUs with a second tool +therefore used to cost almost twice as much as testing them with one, because +each job built its own copy of them. + +Give `--fmisimulator` once per tool and the FMUs are built once and simulated +with each of them: + +```bash +./test.py --branch=v1.27-fmi --fmi=true \ + --fmisimulator=/path/to/OMSimulator \ + --fmisimulator='python3 -m fmpy' \ + configs/myConf.json +``` + +`--branch` names the job; every simulator stores its results in a branch of its +own derived from it, so the run above fills + +| simulator | branch | published to | +| --- | --- | --- | +| OMSimulator | `v1.27-fmi` | `branches/v1.27-fmi` | +| FMPy | `v1.27-fmi-fmpy` | `branches/v1.27-fmi-fmpy` | + +which is where those results have always been. OMSimulator keeps the plain +`-fmi` branch; every other tool adds its name. The branch a tool fills depends +on the tool and not on the order, so asking for FMPy alone still fills +`v1.27-fmi-fmpy` and leaves `v1.27-fmi` alone. + +A branch directory looks the same as it always did, file names included. The +`.err` of a model is written by the build, so every simulator of it publishes +the same one; the `.sim` and the difference files are the ones that simulator +produced. + +Only the simulator may differ between the results that share an FMU. Anything +that changes the FMU itself - a different compiler, a different library, a +different `--fmuType` or `--fmiFlags` - is a different job, which is why the +Co-Simulation jobs with CVODE are not merged with the Model Exchange ones. + +In Jenkins the parameters keep their meaning: `fmi_v1_27` asks for OMSimulator +and `fmpy_fmi_v1_27` for FMPy. Ticking both runs one job that builds every FMU +once and simulates it with both; ticking one runs that tool alone. + +### Adding an FMI simulator + +The simulators live in [configs/fmi-simulators.json](configs/fmi-simulators.json). +Adding one is an entry there and no change to any script: + +```json +"fmusim": { + "resultExtension": "csv", + "versionArgument": "--version", + "optionalArguments": { "stepSizeArgument": " --output-interval {stepSize:g}" }, + "arguments": "--interface-type ModelExchange --output-file {result} --start-time {startTime:g} --stop-time {stopTime:g}{stepSizeArgument} {fmu}" +} +``` + +- `arguments` is the command line, a template over `simulator`, `fmu`, + `result`, `requestedResult`, `tempDir`, `startTime`, `stopTime`, `tolerance`, + `timeout`, `stepSize` and anything named in `optionalArguments`. +- `optionalArguments` are the flags that have to disappear when there is nothing + to put in them. OMSimulator hangs on `--stepSize=0` rather than ignoring it, + so its step size flag lives here, while FMPy wants `--output-interval 0` all + the same and writes the value straight into its `arguments`. +- `command` is how the tool is invoked, `{simulator}` by default. FMPy needs a + subcommand, `{simulator} simulate`. +- `resultExtension` is what the tool writes, so that the comparison against the + reference file knows what to read. +- `versionArgument` prints the version, which is recorded with the results. +- `branchSuffix` overrides the `-` a tool adds to the branch. Only + OMSimulator needs it, with `""`. +- `untested` marks an entry nobody has run yet; the run then says so instead of + failing every model with a puzzling error. Remove it once it works. + +Then run it with `--fmisimulator=fmusim=/path/to/fmusim`, or just +`--fmisimulator=/path/to/fmusim` if the command contains the name. + +A tool that is a Python package rather than a command line needs a small driver +script that takes the arguments its entry passes, simulates, writes the result +file and exits non-zero when it fails; the entry then points `command` at it. + +### Testing a pull request + +A branch is tested against its own previous run, which says what broke *after* a +change was merged. A pull request can be tested before that, against the newest +run of `master`. + +In Jenkins, set the **`pull_request`** parameter to the pull request number and +start the job; `pull_request_baseline`, `pull_request_config` and +`pull_request_node` say what it is compared against, what it tests and where. +None of the branch jobs run unless their own parameter is ticked as well. + +By hand it is two steps. The compiler is built from the merge ref - the pull +request as it would land, not the branch on its own - and the run fills a +`pr-` table like any other branch: + +```bash +git fetch --force https://github.com/OpenModelica/OpenModelica.git refs/pull//merge +git checkout -f --detach FETCH_HEAD +# build omc, then +./test.py --branch=pr- configs/conf.json +``` + +and the report compares that run against the newest run of `master`: + +```bash +./pr-report.py # --baseline=master by default +``` + +It writes `history/pr-/...html`, the same +kind of page as the nightly regression reports, next to `00_comment.md`, a +summary to comment on the pull request with. Both are published with the other +reports. + +Two things make a difference mean something other than "the pull request did +this", and the report says so when they apply: **the machine**, since two runs +produced on different hardware compare the hardware as much as the change, and +**the libraries**, since two runs that tested different library versions, or +verified against different reference files, differ for reasons of their own. The +baseline is also the newest `master` run rather than the commit the pull request +is based on, so a difference can come from anything merged since it was +branched - a reason to rebase before believing a surprising result. + +A full run takes days, so testing every pull request this way is not the idea; +point `--branch=pr-` at a smaller configuration file when the question is +narrower. + +The tables accumulate, about 19500 rows each. `drop-pr-tables.py` drops the ones +whose pull request has been merged or closed, and those tested more than +`--older-than` days ago, together with the rows their runs left in the other +tables; it lists them and does nothing unless it is given `--yes`. The reports +published for them are not touched. + +### Generate HTML results + + ```bash + ./report.py configs/myConf.json + ``` + +- Upload and backup + - Upload HTML files somewhere + - backup sqlite3.db + +### Reference Results + +If you use the default configs `config/conf.json` and +`config/conf-c++.json` to test all libraries you need to +download the reference files and make them available by +defining `MSLREFERENCE` and `REFERENCEFILES`. + +Some result file locations: + +- Modelica Association: [modelica/MAP-LIB_ReferenceResults](https://github.com/modelica/MAP-LIB_ReferenceResults) +- PNLib: [AMIT-HSBI/PNlib](https://github.com/AMIT-HSBI/PNlib) +- DLR-SR: [DLR-SR/ThermoFluidStream-Regression](https://github.com/DLR-SR/ThermoFluidStream-Regression) + and [DLR-SR/PlanarMechanics_ReferenceResults](https://github.com/DLR-SR/PlanarMechanics_ReferenceResults) + +To download the MSL reference files create a file +installReferenceResults.sh with + +```sh +#!/bin/sh + +refdir="/some/path/to/ReferenceFiles" # Change the path! + +# Update git repo for MSL Reference files +mkdir -p $refdir/modelica.org/ReferenceResults +cd $refdir/modelica.org/ReferenceResults +rm -rf $refdir/MAP-LIB_ReferenceResults/ + +test -f MAP-LIB_ReferenceResults.git/config || git clone --bare https://github.com/modelica/MAP-LIB_ReferenceResults.git MAP-LIB_ReferenceResults.git +cd MAP-LIB_ReferenceResults.git +git fetch origin '*:*' +git fetch --tags --force + +for tag in $(git for-each-ref --format="%(refname:lstrip=-1)" refs/heads/) +do + echo "tag: $tag" + base="$refdir/MAP-LIB_ReferenceResults/$tag" + mkdir -p $base + echo "mkdir -p $base" + git archive --format=tar $tag | (cd $base && tar xvf -) +done +``` + +and run it + +```bash +chmod a+rx installReferenceResults.sh +./installReferenceResults.sh +export MSLREFERENCE="/some/path/to/ReferenceFiles/" +``` + +For the other libraries just clone the repositories to `/some/path/to/ReferenceFiles/`. diff --git a/all-plots.py b/all-plots.py index 19917b0..e7c8026 100755 --- a/all-plots.py +++ b/all-plots.py @@ -3,7 +3,7 @@ import sys, argparse, subprocess, os import simplejson as json -import shared +import shared, resultsdb import re, time, math from omcommon import friendlyStr @@ -18,6 +18,7 @@ parser = argparse.ArgumentParser(description='OpenModelica model testing report generation tool') parser.add_argument('branches', nargs='*') parser.add_argument('--historypath', default="history") +resultsdb.addArgument(parser) args = parser.parse_args() branches = [branch.split("/")[-1] for branch in args.branches] @@ -25,11 +26,11 @@ libs = {} -import cgi, sqlite3, time, datetime +import cgi, time, datetime from omcommon import friendlyStr, multiple_replace -conn = sqlite3.connect('sqlite3.db') -cursor = conn.cursor() +db = resultsdb.connect(args.db) +cursor = db.cursor() def dateStr(dint): return str(datetime.datetime.fromtimestamp(dint).strftime('%Y-%m-%d %H:%M:%S')) @@ -98,19 +99,26 @@ def plotLibrary(branch, libname, xs, total, frontend,backend,simcode,template,co for branch in branches: try: - cursor.execute("SELECT name FROM [sqlite_master] WHERE type='table' AND name=?", (branch,)) - v = cursor.fetchone()[0] + one = (branch,) if db.tableExists(branch) else None + if one == None: + print("No such table '%s'; specify it using --branch=XXX when running test.py" % branch) + # ignore this table and continue + continue + else: + v = one[0] except: - raise Exception("No such table '%s'; specify it using --branch=XXX" % branch) - -for branch in branches: - cursor.execute('''CREATE INDEX IF NOT EXISTS [idx_%s_date] ON [%s](date)''' % (branch,branch)) + # raise Exception("No such table '%s'; specify it using --branch=XXX" % branch) + print("No such table '%s'; specify it using --branch=XXX when running test.py" % branch) + # ignore this table and continue + continue + + db.createDateIndex(branch) libs = {} - for (date,libname,total,frontend,backend,simcode,template,compile,simulate,verify) in cursor.execute("""SELECT date,libname,COUNT(finalphase),COUNT(finalphase>=1 or null),COUNT(finalphase>=2 or null),COUNT(finalphase>=3 or null),COUNT(finalphase>=4 or null),COUNT(finalphase>=5 or null),COUNT(finalphase>=6 or null),COUNT(finalphase>=7 or null) - FROM [%s] + for (date,libname,total,frontend,backend,simcode,template,compile,simulate,verify) in cursor.execute("""SELECT date,libname,COUNT(finalphase),%s + FROM %s GROUP BY date,libname ORDER BY libname,date ASC -""" % (branch)): +""" % (",".join(db.countIf("finalphase>=%d" % i) for i in range(1,8)), db.quote(branch))): if libname not in libs: libs[libname] = ([],[],[],[],[],[],[],[],[]) libs[libname][0].append(datetime.datetime.fromtimestamp(date)) diff --git a/all-reports.py b/all-reports.py index b56bf80..c100d42 100755 --- a/all-reports.py +++ b/all-reports.py @@ -5,7 +5,7 @@ import codecs import sys, argparse, subprocess, os, time import simplejson as json -import shared +import shared, resultsdb import re from omcommon import friendlyStr @@ -14,8 +14,10 @@ parser.add_argument('--baseurl', default="http://libraries.openmodelica.org/branches") parser.add_argument('--historyurl', default="http://libraries.openmodelica.org/branches/history") parser.add_argument('--githuburl', default="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/OpenModelica/OpenModelica/commit") +parser.add_argument('--githuburltesting', default="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/OpenModelica/OpenModelicaLibraryTesting/commit") parser.add_argument('--omcgitdir', default="../OpenModelica/OpenModelica") parser.add_argument('--email', default=False, action='store_true') +resultsdb.addArgument(parser) args = parser.parse_args() os.environ['TZ'] = 'Europe/Stockholm' @@ -25,6 +27,7 @@ baseurl = args.baseurl historyurl = args.historyurl githuburl = args.githuburl +githuburltesting = args.githuburltesting omcgitdir = args.omcgitdir doemail = args.email @@ -42,17 +45,18 @@ libs = {} -import cgi, sqlite3, time, datetime +import cgi, time, datetime from omcommon import friendlyStr, multiple_replace -conn = sqlite3.connect('sqlite3.db') -cursor = conn.cursor() +db = resultsdb.connect(args.db) +cursor = db.cursor() def dateStr(dint): return str(datetime.datetime.fromtimestamp(dint).strftime('%Y-%m-%d %H:%M:%S')) def getTagOrVersion(v): v = v.replace("OpenModelica ","").replace("OMCompiler ","") + v = re.sub(r"-rust$", "", v) m = re.search("[+]g([0-9a-f]{7}[0-9a-f]*)$", v) if m: return m.group(1) @@ -64,15 +68,27 @@ def libraryLink(branch, libname): def modelLink(libname, modelname, extension, text): return '%s' % (baseurl,branch,libname,libname,modelname,extension,text) +missing_branches = [] emails_to_send = {} for branch in branches: try: - cursor.execute("SELECT name FROM [sqlite_master] WHERE type='table' AND name=?", (branch,)) - v = cursor.fetchone()[0] + one = (branch,) if db.tableExists(branch) else None + if one == None: + print("No such table '%s'; specify it using --branch=XXX when running test.py" % branch) + # ignore this table and continue + missing_branches.append(branch) + continue + else: + v = one[0] except: - raise Exception("No such table '%s'; specify it using --branch=XXX" % branch) - cursor.execute('''CREATE INDEX IF NOT EXISTS [idx_%s_date] ON [%s](date)''' % (branch,branch)) - cursor.execute("SELECT date,omcversion FROM [omcversion] WHERE branch LIKE ? COLLATE NOCASE ORDER BY date ASC", (branch,)) + #raise Exception("No such table '%s'; specify it using --branch=XXX" % branch) + print("No such table '%s'; specify it using --branch=XXX when running test.py" % branch) + # ignore this table and continue + missing_branches.append(branch) + continue + + db.createDateIndex(branch) + cursor.execute("SELECT date,omcversion FROM omcversion WHERE %s ORDER BY date ASC" % db.likeNoCase("branch"), (branch,)) entries = cursor.fetchall() n = len(entries) urlToOpen = "%s/%s/00_history.html" % (historyurl, branch) @@ -80,7 +96,8 @@ def modelLink(libname, modelname, extension, text): urlContents = urllib.request.urlopen(urlToOpen).read().decode('utf-8') except: print(urlToOpen + " failed to open") - raise + missing_branches.append(branch) + continue generated = False @@ -100,7 +117,7 @@ def modelLink(libname, modelname, extension, text): emails_current = set(["openmodelicabuilds@ida.liu.se"]) if v1 != v2: try: - gitlog = subprocess.check_output(["git", "log", '--pretty=%%h%%an%%s' % (githuburl), "%s..%s" % (v1,v2)], cwd=omcgitdir).decode("utf-8") + gitlog = subprocess.check_output(["git", "log", '--pretty=%%h%%ai%%an%%s' % (githuburl), "%s..%s" % (v1,v2)], cwd=omcgitdir).decode("utf-8") print("Do git ls-tree for %s %s" % (v1,v2)) try: t1 = subprocess.check_output(["git", "ls-tree", v1, "OMCompiler/3rdParty"], cwd=omcgitdir).decode("utf-8").strip().split(" ")[2].split("\t")[0] @@ -119,18 +136,28 @@ def modelLink(libname, modelname, extension, text): for email in [email.strip() for email in subprocess.check_output(["git", "log", '--pretty=%ae', "%s..%s" % (v1,v2)], cwd=omcgitdir).decode("utf-8").split("\n")]: if "@" not in email: continue + # adrpo: if email domain doesn't have "." in it, skip it + if "." not in email[email.find("@"):]: + continue emails_current.add(email) except subprocess.CalledProcessError as e: print(str(e)) gitlog = "%s..%s" % (v1,v2) else: gitlog = "" - tpl = tpl.replace("#OMCGITLOG#",gitlog).replace("#NUMCOMMITS#",str(gitlog.count(""))).replace("#3rdParty#",thirdPartyChanged) - libnames = [libname for (libname,) in cursor.execute("""SELECT libname FROM [%s] WHERE date=? GROUP BY libname""" % branch, (d2,))] + + try: + gitloglibrarytesting = subprocess.check_output(["git", "log", '--pretty=%%h%%ai%%an%%s' % (githuburltesting), "-2"], cwd="./").decode("utf-8") + except subprocess.CalledProcessError as e: + print(str(e)) + gitloglibrarytesting = "could not get the git log for OpenModelicaLibraryTesting" + + tpl = tpl.replace("#OMCGITLOG#",gitlog).replace("#NUMCOMMITS#",str(gitlog.count(""))).replace("#3rdParty#",thirdPartyChanged).replace("#OMCLIBRARYTESTINGGITLOG#",gitloglibrarytesting) + libnames = [libname for (libname,) in cursor.execute("""SELECT libname FROM %s WHERE date=? GROUP BY libname""" % db.quote(branch), (d2,))] startdates = {} # Get previous date of each library run and group them together for fast queries later for libname in libnames: - ds = cursor.execute("""SELECT date FROM [%s] WHERE date MAX(finalphase)) OR ((MIN(finalphase) >= ?) AND (MAX(frontend) > ?*MIN(frontend) AND MAX(frontend) > ?) OR @@ -153,7 +184,7 @@ def modelLink(libname, modelname, extension, text): (MAX(compile) > ?*MIN(compile) AND MAX(compile) > ?) OR (MAX(simulate) > ?*MIN(simulate) AND MAX(simulate) > ?) ) - """ % (branch,",".join(["'%s'" % libname for libname in startdates[d1lib]])) + """) % (db.quote(branch),",".join(["'%s'" % libname for libname in startdates[d1lib]])) cursor.execute(query, (d1lib,d2,timeMinPhase,timeRel,timeAbs,timeRel,timeAbs,timeRel,timeAbs,timeRel,timeAbs,timeRel,2*timeAbs,timeRel,timeAbs)) regressions += cursor.fetchall() regressions = sorted(regressions, key = lambda x: (x[1],x[0])) @@ -202,10 +233,10 @@ def modelLink(libname, modelname, extension, text): libstrs = [] for libname in sorted(list(libs)): - cursor.execute("SELECT libversion,confighash FROM [libversion] WHERE branch LIKE ? COLLATE NOCASE AND date<=? AND libname=? ORDER BY date DESC LIMIT 1", (branch,d1,libname)) + cursor.execute("SELECT libversion,confighash FROM libversion WHERE %s AND date<=? AND libname=? ORDER BY date DESC LIMIT 1" % db.likeNoCase("branch"), (branch,d1,libname)) (lv1,lh1) = cursor.fetchone() lv1 = lv1.strip() - cursor.execute("SELECT libversion,confighash FROM [libversion] WHERE branch LIKE ? COLLATE NOCASE AND date<=? AND libname=? ORDER BY date DESC LIMIT 1", (branch,d2,libname)) + cursor.execute("SELECT libversion,confighash FROM libversion WHERE %s AND date<=? AND libname=? ORDER BY date DESC LIMIT 1" % db.likeNoCase("branch"), (branch,d2,libname)) (lv2,lh2) = cursor.fetchone() lv2 = lv2.strip() if lv1 != lv2: @@ -244,22 +275,34 @@ def modelLink(libname, modelname, extension, text): from email.headerregistry import Address from email.utils import make_msgid +missing_plain = "" +missing_html = "" +if missing_branches: + missing_plain = ", ".join(missing_branches) + missing_plain = "Report asks for missing branches which we ignored: %s\n" % missing_plain + missing_html = ("%s %s %s" % ("

", missing_plain, " + +%s

The following reports contain regressions your account was involved with:

%s -""" % "\n".join(reversed(emails_to_send[email]["html"])), subtype='html') - with smtplib.SMTP('test.openmodelica.org') as s: +""" % (missing_html, "\n".join(reversed(emails_to_send[email]["html"]))), subtype='html') + with smtplib.SMTP('smtp.office365.com') as s: + s.starttls() + s.ehlo() + s.login(os.environ["IDA_EMAIL_USR"],os.environ["IDA_EMAIL_PSW"]) s.send_message(msg) diff --git a/clean-dates.py b/clean-dates.py index ddafe63..09b831e 100755 --- a/clean-dates.py +++ b/clean-dates.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 -import argparse, sqlite3, sys +import argparse, sys +import resultsdb from datetime import datetime parser = argparse.ArgumentParser(description='OpenModelica library testing tool') parser.add_argument('startDate') parser.add_argument('stopDate') +resultsdb.addArgument(parser) args = parser.parse_args() @@ -29,12 +31,12 @@ sys.stdout.write("Please respond with 'yes' or 'no'") sys.exit(1) -conn = sqlite3.connect('sqlite3.db') -cursor = conn.cursor() +db = resultsdb.connect(args.db) +cursor = db.cursor() -tables = [tbl for (tbl,) in cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")] +tables = db.tables() for tbl in tables: - cursor.execute("DELETE FROM [%s] WHERE date?" % tbl, (stopTime.timestamp(),startTime.timestamp())) -conn.commit() -conn.execute("VACUUM") -conn.close() + cursor.execute("DELETE FROM %s WHERE date?" % db.quote(tbl), (stopTime.timestamp(),startTime.timestamp())) +db.commit() +db.vacuum() +db.close() diff --git a/clean-empty-omcversion-dates.py b/clean-empty-omcversion-dates.py index 1b0fef4..63aad13 100755 --- a/clean-empty-omcversion-dates.py +++ b/clean-empty-omcversion-dates.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 -import argparse, sqlite3, sys +import argparse, sys +import resultsdb from datetime import datetime parser = argparse.ArgumentParser(description='OpenModelica library testing tool') +resultsdb.addArgument(parser) args = parser.parse_args() -conn = sqlite3.connect('sqlite3.db') -cursor = conn.cursor() +db = resultsdb.connect(args.db) +cursor = db.cursor() entries = cursor.execute("SELECT date,branch FROM omcversion").fetchall() dropped=0 @@ -20,7 +22,11 @@ branchDates[branch] = set() branchDates[branch].add(date) for branch in branches: - data=cursor.execute("SELECT DISTINCT date FROM [%s]" % branch).fetchall() + # The shared database holds the branches of every machine, including ones + # this one never created a result table for. + if not db.tableExists(branch): + continue + data=cursor.execute("SELECT DISTINCT date FROM %s" % db.quote(branch)).fetchall() for (date,) in data: try: branchDates[branch].remove(date) @@ -28,10 +34,10 @@ pass for date in branchDates[branch]: print("Dropping empty omcversion entry (%d,%s)" % (date,branch)) - cursor.execute("DELETE FROM [omcversion] WHERE date=? AND branch=?", (date,branch)) + cursor.execute("DELETE FROM omcversion WHERE date=? AND branch=?", (date,branch)) dropped += 1 -conn.commit() +db.commit() if dropped>0: - conn.execute("VACUUM") -conn.close() + db.vacuum() +db.close() diff --git a/conf-howto.md b/conf-howto.md new file mode 100644 index 0000000..e832bbc --- /dev/null +++ b/conf-howto.md @@ -0,0 +1,20 @@ +# How to set up the library testing configuration file + +The [conf.json](configs/conf.json) file specifies which versions of the libraries included in the package manager are ran, and with which options. +For each test entry, there are several fields: +- `library`: name of the library +- `libraryVersion`: version of the library; this is the content of the library version annotation for tagged versions, or the name of the branch + as specified in one of the `branches` field of the Package Manager server [repos.json](https://github.com/OpenModelica/OMPackageManager/blob/master/repos.json) file, + in case a non-tagged version needs to be tested. If no version is provided, the latest tagged version is tested automatically +- `libraryVersionNameForTests`: this is appended with an underscore to the name of the library in the test report name +- `ignoreModelPrefix`: skips testing all models that start with that string + +There are many other fields to manage reference results, that can be either stored locally on the server, or fetched from a remote GIT repository, +and for providing extra flags and settings to OMC when running the tests. Please refer to [conf.json](configs/conf.json) for more examples, and to +the [master result directory](https://libraries.openmodelica.org/branches/master/) for an example of the corresponding output. Please +note that some files and/or directory there may be leftovers of old test runs, only consider recent ones if you want to understand what the outcome +of a certain test configuration is. + +Note that in order to test a library it is also necessary to +- include it in the package manager (see the [OMPackageManager Instructions](https://github.com/OpenModelica/OMPackageManager#readme)), where it is possible to configure branches that may be tested besides the tagged released versions, e.g. the master branch or some maintenance branch +- have it installed by the CI. In order to do that, it needs to be added to the [installLibraries.mos](https://github.com/OpenModelica/OpenModelicaLibraryTesting/blob/master/.CI/installLibraries.mos) file. diff --git a/configs/basemodelica.json b/configs/basemodelica.json new file mode 100644 index 0000000..27deb59 --- /dev/null +++ b/configs/basemodelica.json @@ -0,0 +1,12 @@ +[ + { + "library":"Modelica", + "libraryVersion":"4.1.0", + "libraryVersionExactMatch":true, + "referenceFileExtension":"csv", + "referenceFileNameDelimiter":"/", + "referenceFileNameExtraName":"$ClassName", + "referenceFiles":"$MSLREFERENCE/MAP-LIB_ReferenceResults/v4.1.0", + "optlevel":"-Os -march=native" + } +] diff --git a/configs/conf-c++.json b/configs/conf-c++.json index c011cb9..85d34ea 100644 --- a/configs/conf-c++.json +++ b/configs/conf-c++.json @@ -67,6 +67,10 @@ "configExtraName":"cpp", "simCodeTarget":"Cpp", "alarmFlag":"--alarm", - "abortSlowSimulation":"" + "abortSlowSimulation":"", + "referenceFiles":{ + "giturl":"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/rfranke/PowerSystems_ReferenceResults", + "destination":"ReferenceFiles/PowerSystems" + } } ] diff --git a/configs/conf-nonstandard.json b/configs/conf-nonstandard.json index 435f885..96fe190 100644 --- a/configs/conf-nonstandard.json +++ b/configs/conf-nonstandard.json @@ -1,6 +1,18 @@ [ { - "library":"Modelica_LinearSystems2" + "library":"FCSys" + }, + { + "library":"IdealizedContact" + }, + { + "library":"Modelica_LinearSystems2", + "libraryVersionNameForTests":"", + "libraryVersion":"master" + }, + { + "library":"Modelica_LinearSystems2", + "libraryVersion":"dev" }, { "library":"Modelica_StateGraph2" diff --git a/configs/conf-old.json b/configs/conf-old.json index 4483c4a..fe6437d 100644 --- a/configs/conf-old.json +++ b/configs/conf-old.json @@ -3,38 +3,9 @@ "library":"Annex60", "libraryVersion":"1.0.0" }, - { - "library":"Buildings", - "libraryVersion":"1.6" - }, - { - "library":"Buildings", - "libraryVersion":"3.0.0" - }, { "library":"iPSL" }, - { - "library":"Modelica", - "libraryVersion":"3.1" - }, - { - "library":"Modelica", - "libraryVersion":"3.2.2", - "optlevel":"-Os -march=native" - }, - { - "library":"ModelicaTest", - "libraryVersion":"3.2.2" - }, - { - "library":"Modelica", - "libraryVersion":"3.2.1" - }, - { - "library":"ModelicaTest", - "libraryVersion":"3.2.1" - }, { "library":"ObjectStab" }, diff --git a/configs/conf.json b/configs/conf.json index 88bef18..0999e8c 100644 --- a/configs/conf.json +++ b/configs/conf.json @@ -5,13 +5,23 @@ "referenceFileExtension":"csv", "referenceFileNameDelimiter":"/", "referenceFileNameExtraName":"$ClassName", - "referenceFiles":"$MSLREFERENCE/MAP-LIB_ReferenceResults/v4.0.0", - "extraCustomCommands":["if not setCommandLineOptions(\"--std=3.3\") then exit(1); end if;"], + "referenceFiles":"$MSLREFERENCE/MAP-LIB_ReferenceResults/v4.1.0", + "optlevel":"-Os -march=native" + }, + { + "library":"Modelica", + "libraryVersion":"4.1.0", + "libraryVersionExactMatch":true, + "referenceFileExtension":"csv", + "referenceFileNameDelimiter":"/", + "referenceFileNameExtraName":"$ClassName", + "referenceFiles":"$MSLREFERENCE/MAP-LIB_ReferenceResults/v4.1.0", "optlevel":"-Os -march=native" }, { "library":"Modelica", "libraryVersion":"4.0.0", + "libraryVersionExactMatch":true, "referenceFileExtension":"csv", "referenceFileNameDelimiter":"/", "referenceFileNameExtraName":"$ClassName", @@ -36,8 +46,16 @@ "referenceFileExtension":"csv", "referenceFileNameDelimiter":"/", "referenceFileNameExtraName":"$ClassName", - "referenceFiles":"$MSLREFERENCE/MAP-LIB_ReferenceResults/v4.0.0", - "extraCustomCommands":["if not setCommandLineOptions(\"--std=3.3\") then exit(1); end if;"], + "referenceFiles":"$MSLREFERENCE/MAP-LIB_ReferenceResults/v4.1.0", + "optlevel":"-Os -march=native" + }, + { + "library":"ModelicaTest", + "libraryVersion":"4.1.0", + "referenceFileExtension":"csv", + "referenceFileNameDelimiter":"/", + "referenceFileNameExtraName":"$ClassName", + "referenceFiles":"$MSLREFERENCE/MAP-LIB_ReferenceResults/v4.1.0", "optlevel":"-Os -march=native" }, { @@ -47,7 +65,6 @@ "referenceFileNameDelimiter":"/", "referenceFileNameExtraName":"$ClassName", "referenceFiles":"$MSLREFERENCE/MAP-LIB_ReferenceResults/v4.0.0", - "extraCustomCommands":["if not setCommandLineOptions(\"--std=3.3\") then exit(1); end if;"], "optlevel":"-Os -march=native" }, { @@ -62,13 +79,6 @@ { "library":"ModelicaTestOverdetermined" }, - { - "library":"Modelica_Synchronous", - "libraryVersion":"master", - "libraryVersionNameForTests":"", - "extraCustomCommands":["if not setCommandLineOptions(\"--std=3.3\") then exit(1); end if;"], - "referenceFiles":"$REFERENCEFILES/Modelica_Synchronous" - }, { "library":"Modelica_DeviceDrivers", "libraryVersion":"master", @@ -76,12 +86,38 @@ "extraCustomCommands":["if not setCommandLineOptions(\"--std=3.3\") then exit(1); end if;"] }, { - "library":"Modelica_Noise", - "libraryVersion":"master", - "libraryVersionNameForTests":"" + "library":"PowerGrids", + "referenceFileExtension":"mat", + "referenceFileNameDelimiter":".", + "referenceFinalDot":"_res.", + "referenceFiles":{ + "giturl":"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/PowerGrids/PowerGrids-ref", + "destination":"ReferenceFiles/PowerGrids" + } }, { - "library":"PowerGrids" + "library":"PowerGrids", + "libraryVersionNameForTests":"symb_jac", + "extraCustomCommands":["setCommandLineOptions(\"--generateDynamicJacobian=symbolic\")"], + "referenceFileExtension":"mat", + "referenceFileNameDelimiter":".", + "referenceFinalDot":"_res.", + "referenceFiles":{ + "giturl":"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/PowerGrids/PowerGrids-ref", + "destination":"ReferenceFiles/PowerGrids" + } + }, + { + "library":"PowerGrids", + "libraryVersion":"develop", + "libraryVersionNameForTests":"dev", + "referenceFileExtension":"mat", + "referenceFileNameDelimiter":".", + "referenceFinalDot":"_res.", + "referenceFiles":{ + "giturl":"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/PowerGrids/PowerGrids-ref", + "destination":"ReferenceFiles/PowerGrids" + } }, { "library":"PowerSystems", @@ -93,7 +129,11 @@ "library":"PowerSystems", "libraryVersion":"master", "libraryVersionNameForTests":"latest", - "optlevel":"-Os -march=native" + "optlevel":"-Os -march=native", + "referenceFiles":{ + "giturl":"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/rfranke/PowerSystems_ReferenceResults", + "destination":"ReferenceFiles/PowerSystems" + } }, { "library":"AdvancedNoise", @@ -103,13 +143,11 @@ { "library":"AES", "libraryVersion":"main", - "libraryVersionNameForTests":"" + "libraryVersionNameForTests":"", + "extraCustomCommands":["setCommandLineOptions(\"--allowNonStandardModelica=illegalConditionalContext\");"] }, { - "library":"AixLib", - "libraryVersion":"master", - "libraryVersionNameForTests":"latest", - "forceNewHash":20210701 + "library":"AixLib" }, { "library":"IBPSA", @@ -117,23 +155,56 @@ "libraryVersionNameForTests":"latest", "extraCustomCommands":["setCommandLineOptions(\"-d=ignoreCycles\")"] }, + { + "library":"BidiDCDC", + "libraryVersionNameForTests":"" + }, + { + "library":"BLDC", + "libraryVersionNameForTests":"" + }, + { + "library":"BoundaryInitBlocks", + "libraryVersion":"master", + "libraryVersionNameForTests":"" + }, { "library":"Buildings", - "ignoreModelPrefix":"Buildings.Utilities.IO.BCVTB.Examples", - "libraryVersion":"maint.7.0.x", - "libraryVersionLatestInPackageManager":true + "ignoreModelPrefix":["Buildings.Utilities.IO.BCVTB.Examples", "Buildings.Obsolete"], + "libraryVersion":"maint.11.x", + "libraryVersionNameForTests":"11", + "ulimitOmc":300, + "ulimitExe":400, + "runOnceBeforeTesting":[["$resourceLocation/src/ThermalZones/install.py", "--binaries-for-os-only"]], + "referenceFileExtension":"csv", + "referenceFileNameDelimiter":"_", + "referenceFiles":"/mnt/ReferenceFiles/Buildings/csv/maint_11.x", + "extraCustomCommands":["setCFlags(getCFlags() + \" -Wno-error=implicit-function-declaration\")"] }, { "library":"Buildings", - "ignoreModelPrefix":"Buildings.Utilities.IO.BCVTB.Examples", - "libraryVersion":"8.0.0", - "libraryVersionLatestInPackageManager":true + "ignoreModelPrefix":["Buildings.Utilities.IO.BCVTB.Examples", "Buildings.Obsolete"], + "libraryVersion":"maint.12.x", + "libraryVersionNameForTests":"12", + "ulimitOmc":300, + "ulimitExe":400, + "runOnceBeforeTesting":[["$resourceLocation/src/ThermalZones/install.py", "--binaries-for-os-only"]], + "referenceFileExtension":"csv", + "referenceFileNameDelimiter":"_", + "referenceFiles":"/mnt/ReferenceFiles/Buildings/csv/maint_12.x", + "extraCustomCommands":["setCFlags(getCFlags() + \" -Wno-error=implicit-function-declaration\")"] }, { "library":"Buildings", - "ignoreModelPrefix":"Buildings.Utilities.IO.BCVTB.Examples", + "ignoreModelPrefix":["Buildings.Utilities.IO.BCVTB.Examples", "Buildings.Obsolete"], "libraryVersion":"master", - "libraryVersionNameForTests":"latest" + "libraryVersionNameForTests":"latest", + "ulimitOmc":300, + "ulimitExe":800, + "runOnceBeforeTesting":[["$resourceLocation/src/ThermalZones/install.py", "--binaries-for-os-only"]], + "referenceFileExtension":"csv", + "referenceFileNameDelimiter":"_", + "referenceFiles":"/mnt/ReferenceFiles/Buildings/csv/master" }, { "library":"BuildingSystems", @@ -141,7 +212,17 @@ "libraryVersionNameForTests":"" }, { - "library":"BuildSysPro" + "library":"BuildSysPro", + "_comment":"IncludeDirectory says modelica://BuildSysPro/Resources/C-Sources, which does not exist; the IBPSA C sources are in IBPSA/Resources/C-Sources (Resources/IBPSA/C-Sources in 3.5.0 and older). Some of them call str*/malloc/ModelicaError without including the header", + "extraCustomCommands":["setCFlags(getCFlags() + \" -include string.h -include stdlib.h -include ModelicaUtilities.h \\\"-I$libraryLocation/IBPSA/Resources/C-Sources\\\" \\\"-I$libraryLocation/Resources/IBPSA/C-Sources\\\"\")"] + }, + { + "library":"BusinessSimulation" + }, + { + "library":"BusinessSimulation", + "libraryVersion":"main", + "libraryVersionNameForTests":"dev" }, { "library":"BioChem" @@ -150,13 +231,82 @@ "library":"Chemical" }, { - "library":"ClaRa" + "library":"Chemical", + "libraryVersion":"master", + "libraryVersionNameForTests":"master" }, { - "library":"ConPNlib", - "libraryVersion":"master", + "library":"ClaRa", + "ulimitOmc":800, + "ulimitExe":800, + "referenceFileExtension":"mat", + "referenceFileNameDelimiter":".", + "referenceFiles":{ + "giturl":"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/xrg-simulation/ClaRa-official", + "destination":"ReferenceFiles/ClaRa", + "git-ref": "main", + "git-directory": "ReferenceResults" + } + }, + { + "library":"ClaRa", + "ulimitOmc":800, + "ulimitExe":800, + "libraryVersion":"main", + "libraryVersionNameForTests":"dev", + "referenceFileExtension":"mat", + "referenceFileNameDelimiter":".", + "referenceFiles":{ + "giturl":"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/xrg-simulation/ClaRa-official", + "destination":"ReferenceFiles/ClaRa", + "git-ref": "main", + "git-directory": "ReferenceResults" + } + }, + { + "library":"Credibility" + }, + { + "library":"DeltaRobot" + }, + { + "library":"DroneLibrary", "libraryVersionNameForTests":"" }, + { + "library":"Dynawo", + "extraCustomCommands":["setCommandLineOptions(\"--allowNonStandardModelica=implicitParameterStartAttribute,illegalConditionalContext\");"] + }, + { + "library":"Dynawo", + "libraryVersion":"master", + "libraryVersionNameForTests":"dev", + "extraCustomCommands":["setCommandLineOptions(\"--allowNonStandardModelica=implicitParameterStartAttribute,illegalConditionalContext\");"], + "referenceFileExtension":"csv", + "referenceFileNameDelimiter":".", + "referenceFinalDot":"_res.", + "referenceFiles":{ + "giturl":"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/dynawo/dynawo", + "destination":"ReferenceFiles/Dynawo", + "git-directory": "dynawo/sources/Models/Modelica/Dynawo/Examples/references" + } + }, + { + "library":"EHPTexamples" + }, + { + "library":"EHPTexamples", + "libraryVersion":"main", + "libraryVersionNameForTests":"latest" + }, + { + "library":"EHPTlib" + }, + { + "library":"EHPTlib", + "libraryVersion":"main", + "libraryVersionNameForTests":"latest" + }, { "library":"ElectricalEnergyStorage" }, @@ -164,38 +314,67 @@ "library":"ExtendedPetriNets" }, { - "library":"ExternData" + "library":"ExternalMedia", + "ignoreModelPrefix":"ExternalMedia.Test.FluidProp" }, { - "library":"ExternalMemoryLib" + "library":"ExternData" }, { "library":"FastBuildings" }, { - "library":"FCSys" + "library":"FluxTubesExtension", + "libraryVersion":"main", + "libraryVersionNameForTests":"" + }, + { + "library":"Greenhouses", + "libraryVersion":"master", + "libraryVersionNameForTests":"" }, { "library":"HanserModelica" }, { "library":"HelmholtzMedia", - "libraryVersion":"master", + "libraryVersion":"main", "libraryVersionNameForTests":"" }, { "library":"IndustrialControlSystems" }, { - "library":"IdealizedContact" + "library":"KeyWordIO" }, { - "library":"KeyWordIO" + "library":"IDEAS" + }, + { + "library":"IDEAS", + "libraryVersion":"master", + "libraryVersionNameForTests":"dev" + }, + { + "library":"LEPSE", + "libraryVersion":"main", + "referenceFileExtension":"mat", + "referenceFileNameDelimiter":".", + "referenceFinalDot":"_res.", + "referenceFiles":{ + "giturl":"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/AndrejFlorinskii/LEPSE-ref", + "destination":"ReferenceFiles/LEPSE", + "git-ref": "main" + } }, { "library":"LibRAS", "extraCustomCommands":["if not setCommandLineOptions(\"--std=3.3\") then exit(1); end if;"] }, + { + "library":"MagLev", + "libraryVersionNameForTests":"" + }, { "library":"MEV" }, @@ -205,20 +384,42 @@ { "library":"ModelicaByExample" }, + { + "library":"NeuralNetwork", + "libraryVersion": "main", + "referenceFileExtension":"csv", + "referenceFileNameDelimiter":".", + "referenceFiles":{ + "giturl":"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/AMIT-HSBI/NeuralNetwork", + "destination":"ReferenceFiles/NeuralNetwork" + } + }, { "library":"OpenHydraulics", "libraryVersion":"master", "libraryVersionNameForTests":"" }, + { + "library":"OpenIMDML", + "libraryVersion":"main" + }, { "library":"OpenIPSL", - "libraryVersion":"master", - "libraryVersionNameForTests":"" + "libraryVersion":"2.0.0" }, { "library":"OpenIPSL", "libraryVersion":"", - "libraryVersionNameForTests":"release" + "libraryVersionNameForTests":"", + "libraryVersionLatestInPackageManager":true + }, + { + "library":"OpenIPSL", + "libraryVersion":"master", + "libraryVersionNameForTests":"dev" + }, + { + "library":"Pharmacolibrary" }, { "library":"PhotoVoltaics" @@ -236,7 +437,9 @@ "library":"Physiolibrary" }, { - "library":"Physiomodel" + "library":"Physiomodel", + "libraryVersion":"master", + "libraryVersionNameForTests":"" }, { "library":"PlanarMechanics" @@ -254,10 +457,11 @@ { "library":"ScalableTestGrids", "configExtraName":"noopt", - "ulimitOmc":500, - "ulimitExe":500, + "ignoreModelPrefix":"ScalableTestGrids.Models.Type1Large", + "ulimitOmc":600, "ulimitMemory":12582912, - "optlevel":"-O0" + "optlevel":"-O0", + "extraCustomCommands":["setCommandLineOptions(\"--generateDynamicJacobian=symbolic\")"] }, { "library":"ScalableTestSuite", @@ -294,6 +498,14 @@ { "library":"SolarTherm" }, + { + "library":"SOFCPoliMi", + "libraryVersion":"master", + "libraryVersionNameForTests":"" + }, + { + "library":"StewartPlatform" + }, { "library":"SystemDynamics", "libraryVersion":"master", @@ -315,7 +527,7 @@ { "library":"ThermofluidStream", "libraryVersion":"main", - "libraryVersionNameForTests":"", + "libraryVersionNameForTests":"dev", "ulimitExe":1200, "referenceFileExtension":"mat", "referenceFileNameDelimiter":".", @@ -325,13 +537,12 @@ }, { "library":"ThermofluidStream", - "libraryVersion":"OM_adaptions", "ulimitExe":1200, "referenceFileExtension":"mat", "referenceFileNameDelimiter":".", "referenceFinalDot":"_ref.", "allReferenceFilesExist":false, - "referenceFiles":"$THERMOFLUIDSTREAMREFSOM" + "referenceFiles":"$THERMOFLUIDSTREAMREFS" }, { "library":"ThermalSeparation", @@ -339,12 +550,28 @@ }, { "library":"TILMedia", + "libraryVersion":"main", + "libraryVersionNameForTests":"", "extraLibraries":[ ["ClaRa", "default"] ] }, + { + "library":"TILMedia_AddOn_Tests", + "libraryVersion":"main", + "libraryVersionNameForTests":"", + "extraLibraries":[ + ["TILMedia", "default"], + ["ClaRa", "default"] + ] + }, + { + "library":"ThermoPower", + "optlevel":"-Os -march=native" + }, { "library":"ThermoPower", + "libraryVersion":"4.0.0-dev", "optlevel":"-Os -march=native" }, { @@ -352,6 +579,37 @@ "libraryVersion":"master", "libraryVersionNameForTests":"" }, + { + "library":"TransiEnt", + "libraryVersion":"master", + "libraryVersionNameForTests":"", + "extraCustomCommands": + [ + "setCommandLineOptions(\"--allowNonStandardModelica=nonStdEnumerationAsIntegers,nonStdTopLevelOuter\")", + "setEnvironmentVar(\"publicData\", \"$libraryLocation/Tables/\")", + "setEnvironmentVar(\"privateData\", \"$libraryLocation/Tables/\")", + "setEnvironmentVar(\"superstructureTables\", \"$libraryLocation/Tables/superstructure/Tables/\")", + "setEnvironmentVar(\"superstructureInput\", \"$libraryLocation/Tables/superstructure/RegionInformation/\")" + ], + "environmentSimulation": + [ + ["publicData", "$libraryLocation/Tables/"], + ["privateData", "$libraryLocation/Tables/"], + ["superstructureTables", "$libraryLocation/Tables/superstructure/Tables/"], + ["superstructureInput", "$libraryLocation/Tables/superstructure/RegionInformation/"] + ] + }, + { + "library":"URDFModelica", + "libraryVersion":"main", + "referenceFileExtension":"csv", + "referenceFileNameDelimiter":"/", + "referenceFileNameExtraName":"$ClassName", + "referenceFiles":{ + "giturl":"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/DLR-RM/urdfmodelica-referenceresults", + "destination":"ReferenceFiles/URDFModelica" + } + }, { "library":"VehicleInterfaces" } diff --git a/configs/fmi-simulators.json b/configs/fmi-simulators.json new file mode 100644 index 0000000..ee98939 --- /dev/null +++ b/configs/fmi-simulators.json @@ -0,0 +1,46 @@ +{ + "_comment": [ + "The FMI simulators the testing knows how to run. Adding one is an entry here and nothing else.", + "", + "arguments the command line, a template over: simulator, fmu, result, tempDir,", + " startTime, stopTime, tolerance, timeout, stepSize, and every name", + " defined in optionalArguments.", + "optionalArguments flags that have to disappear when there is nothing to put in them, as a", + " result is always the file the tool should write; requestedResult is empty", + " when the run wants no result file at all, which only OMSimulator acts on.", + " name and its own template. OMSimulator crashes on --stepSize=0 instead of", + " ignoring it, so its step size flag lives here; FMPy wants --output-interval 0", + " all the same and writes the value straight into arguments.", + "command how the tool is invoked, {simulator} by default; FMPy needs a subcommand.", + "resultExtension what the tool writes, so the verification knows what to compare.", + "versionArgument the flag that prints the version, recorded with the results.", + "branchSuffix appended to --branch to get the table of this simulator; the default is", + " -, so OMSimulator is the only one that needs to say anything here.", + "untested set it while nobody has run the entry yet; the run then says so." + ], + "OMSimulator": { + "branchSuffix": "", + "resultExtension": "mat", + "versionArgument": "-v", + "arguments": "{resultArgument} --tempDir={tempDir} --startTime={startTime:g} --stopTime={stopTime:g}{stepSizeArgument} --timeout={timeout:g} --tolerance={tolerance:g} {fmu}", + "optionalArguments": { + "resultArgument": "-r={requestedResult}", + "stepSizeArgument": " --stepSize={stepSize:g}" + } + }, + "fmpy": { + "resultExtension": "csv", + "versionArgument": "--version", + "command": "{simulator} simulate", + "arguments": "--output-file {result} --start-time {startTime:g} --stop-time {stopTime:g} --timeout {timeout:g} --relative-tolerance {tolerance:g} --interface-type ModelExchange --solver CVode --output-interval {stepSize:g} {fmu}" + }, + "fmusim": { + "untested": true, + "resultExtension": "csv", + "versionArgument": "--version", + "arguments": "--interface-type ModelExchange --output-file {result} --start-time {startTime:g} --stop-time {stopTime:g}{stepSizeArgument} {fmu}", + "optionalArguments": { + "stepSizeArgument": " --output-interval {stepSize:g}" + } + } +} diff --git a/configs/heavy_tests.json b/configs/heavy_tests.json new file mode 100644 index 0000000..a45be52 --- /dev/null +++ b/configs/heavy_tests.json @@ -0,0 +1,64 @@ +[ + { + "library":"ScalableTestSuite", + "libraryVersion":"master", + "libraryVersionNameForTests":"OB", + "ulimitOmc":400, + "ulimitExe":300, + "ulimitMemory":62914560, + "procOMC":0, + "procCCompile":0, + "optlevel":"-O0 -march=native", + "referenceFileExtension":"mat", + "referenceFileNameDelimiter":".", + "referenceFinalDot":"_res.", + "referenceFiles":{ + "giturl":"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/casella/ScalableTestSuite-ref", + "destination":"ReferenceFiles/ScalableTestSuite" + } + }, + { + "library":"ScalableTestSuite", + "libraryVersion":"master", + "libraryVersionNameForTests":"NB", + "ulimitOmc":400, + "ulimitExe":300, + "ulimitMemory":62914560, + "procOMC":0, + "procCCompile":0, + "optlevel":"-O0 -march=native", + "referenceFileExtension":"mat", + "referenceFileNameDelimiter":".", + "referenceFinalDot":"_res.", + "referenceFiles":{ + "giturl":"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/casella/ScalableTestSuite-ref", + "destination":"ReferenceFiles/ScalableTestSuite" + }, + "extraCustomCommands":["setCommandLineOptions(\"--newBackend -d=arrayConnect\")"] + }, + { + "library":"LargeTestSuite", + "libraryVersion":"master", + "libraryVersionNameForTests":"NB", + "ulimitOmc":400, + "ulimitExe":400, + "ulimitMemory":62914560, + "procOMC":0, + "procCCompile":0, + "optlevel":"-O0 -march=native", + "extraCustomCommands":["setCommandLineOptions(\"--newBackend -d=arrayConnect\")"] + }, + { + "library":"ScalableTestGrids", + "libraryVersion":"master", + "libraryVersionNameForTests":"NB_SymbolicJacobian", + "ignoreModelPrefix":"ScalableTestGrids.Models.Type1Large", + "ulimitOmc":800, + "ulimitExe":300, + "ulimitMemory":62000000, + "procOMC":0, + "procCCompile":0, + "optlevel":"-O0 -march=native", + "extraCustomCommands":["setCommandLineOptions(\"--newBackend -d=mergeComponents --generateDynamicJacobian=symbolic --maxSizeNonlinearTearing=400\")"] + } +] diff --git a/configs/heavy_tests.json.bak b/configs/heavy_tests.json.bak new file mode 100644 index 0000000..3d9093f --- /dev/null +++ b/configs/heavy_tests.json.bak @@ -0,0 +1,102 @@ +[ + { + "library":"ScalableTestSuite", + "libraryVersion":"master", + "libraryVersionNameForTests":"OB", + "ulimitOmc":400, + "ulimitExe":300, + "ulimitMemory":62914560, + "procOMC":0, + "procCCompile":0, + "optlevel":"-O0 -march=native", + "referenceFileExtension":"mat", + "referenceFileNameDelimiter":".", + "referenceFinalDot":"_res.", + "referenceFiles":{ + "giturl":"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/casella/ScalableTestSuite-ref", + "destination":"ReferenceFiles/ScalableTestSuite" + } + }, + { + "library":"ScalableTestSuite", + "libraryVersion":"master", + "libraryVersionNameForTests":"NB", + "ulimitOmc":400, + "ulimitExe":300, + "ulimitMemory":62914560, + "procOMC":0, + "procCCompile":0, + "optlevel":"-O0 -march=native", + "referenceFileExtension":"mat", + "referenceFileNameDelimiter":".", + "referenceFinalDot":"_res.", + "referenceFiles":{ + "giturl":"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/casella/ScalableTestSuite-ref", + "destination":"ReferenceFiles/ScalableTestSuite" + }, + "extraCustomCommands":["setCommandLineOptions(\"--newBackend -d=arrayConnect\")"] + }, + { + "library":"LargeTestSuite", + "libraryVersion":"master", + "libraryVersionNameForTests":"NB", + "ulimitOmc":400, + "ulimitExe":400, + "ulimitMemory":62914560, + "procOMC":0, + "procCCompile":0, + "optlevel":"-O0 -march=native", + "extraCustomCommands":["setCommandLineOptions(\"--newBackend -d=arrayConnect\")"] + }, + { + "library":"ScalableTestGrids", + "libraryVersion":"master", + "libraryVersionNameForTests":"NB_NumericalJacobian", + "ignoreModelPrefix":"ScalableTestGrids.Models.Type1Large", + "ulimitOmc":800, + "ulimitExe":300, + "ulimitMemory":62000000, + "procOMC":0, + "procCCompile":0, + "optlevel":"-O0 -march=native", + "extraCustomCommands":["setCommandLineOptions(\"--newBackend -d=mergeComponents\")"] + }, + { + "library":"ScalableTestGrids", + "libraryVersion":"master", + "libraryVersionNameForTests":"NB_SymbolicJacobian", + "ignoreModelPrefix":"ScalableTestGrids.Models.Type1Large", + "ulimitOmc":800, + "ulimitExe":300, + "ulimitMemory":62000000, + "procOMC":0, + "procCCompile":0, + "optlevel":"-O0 -march=native", + "extraCustomCommands":["setCommandLineOptions(\"--newBackend -d=mergeComponents --generateDynamicJacobian=symbolic\")"] + }, + { + "library":"ScalableTestGrids", + "libraryVersion":"master", + "libraryVersionNameForTests":"OB_NumericalJacobian", + "ignoreModelPrefix":"ScalableTestGrids.Models.Type1Large", + "ulimitOmc":800, + "ulimitExe":300, + "ulimitMemory":62000000, + "procOMC":0, + "procCCompile":0, + "optlevel":"-O0 -march=native"] + }, + { + "library":"ScalableTestGrids", + "libraryVersion":"master", + "libraryVersionNameForTests":"OB_SymbolicJacobian", + "ignoreModelPrefix":"ScalableTestGrids.Models.Type1Large", + "ulimitOmc":800, + "ulimitExe":300, + "ulimitMemory":62000000, + "procOMC":0, + "procCCompile":0, + "optlevel":"-O0 -march=native", + "extraCustomCommands":["setCommandLineOptions(\"--generateDynamicJacobian=symbolic\")"] + } +] diff --git a/configs/sanityCheck.json b/configs/sanityCheck.json new file mode 100644 index 0000000..fb108ec --- /dev/null +++ b/configs/sanityCheck.json @@ -0,0 +1,28 @@ +[ + { + "library": "MyLibrary", + "libraryVersion": "1.0.0", + "loadFileCommands": [ + "loadFile(\"sanityCheck/MyLibrary/package.mo\")" + ], + "referenceFileExtension": "csv", + "referenceFileNameDelimiter": ".", + "referenceFiles": "$MY_SANITY_CHECK_DIRECTORY/ReferenceFiles" + }, + { + "library": "MyOtherLibrary", + "ignoreModelPrefix":["MyOtherLibrary.Mechanics.MultiBody.Examples.Pendulum", "Some.Other,NonExistentPath"], + "libraryVersion": "1.0.0", + "loadFileCommands": [ + "loadFile(\"sanityCheck/MyOtherLibrary/package.mo\")" + ], + "referenceFileExtension": "csv", + "referenceFileNameDelimiter": ".", + "referenceFiles":{ + "giturl":"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/OpenModelica/OMLibraryTestingTests", + "destination":"ReferenceFiles/MyOtherLibrary", + "git-ref": "main", + "git-directory": "ReferenceResults" + } + } +] diff --git a/conversionscript.py b/conversionscript.py index 9f2536b..e572cf6 100755 --- a/conversionscript.py +++ b/conversionscript.py @@ -68,15 +68,19 @@ def convertPackage(p): latin1Data = fin.read() with open(os.path.join(root, file), "w", encoding="UTF-8") as fout: fout.write(latin1Data) - + if libname in ["Modelica", "ModelicaServices", "Complex", "ModelicaTest", "ModelicaTestOverdetermined"]: ver = libnameOnFile.split(" ")[1] if ver.startswith("1.") or ver.startswith("3."): return None + if libname in ["TAeZoSysPro_testsuite"] and "Modelica" not in uses: + uses['Modelica'] = '3.2.3' + data['uses'] = uses if not uses.get('Modelica','0.0.0') in convertFromVersion: return None if libname in ["Modelica", "ModelicaServices", "Complex", "ModelicaTest", "ModelicaTestOverdetermined"]: return None + print("Start working on %s" % libnameOnFile) omc = OMCSessionZMQ() libnameOnFileFullPath = "converted-libraries/.openmodelica/libraries/%s/package.mo" % libnameOnFile omcAssert(omc, 'loadFile("%s", uses=false)' % libnameOnFileFullPath) @@ -88,7 +92,8 @@ def convertPackage(p): raise Exception("Expected to have loaded %s but got %s" % (libnameOnFileFullPath, loadedFilePath)) gcProfStatsBeforeConversion = omc.sendExpression("GC_get_prof_stats()") timeBeforeConvert = time.time() - omcAssert(omc, 'convertPackage(%s, "%s")' % (libname, conversionScript)) + omcAssert(omc, 'runConversionScript(%s, "%s")' % (libname, conversionScript)) + print("runConversionScript(%s, %s) OK" % (libnameOnFile, conversionScript)) uses = data["uses"] for (n,v) in data["uses"].items(): if n in ["Modelica", "ModelicaServices", "Complex"]: @@ -133,6 +138,8 @@ def convertPackage(p): omc.sendExpression('writeFile("%s", after)' % newFile) if allowErrorsInDiff: errorsInDiff += [newFile] + omc.sendExpression('writeFile("%s.before",before)' % newFile) + omc.sendExpression('writeFile("%s.after",after)' % newFile) res = after nFail += 1 isFail = True @@ -140,7 +147,7 @@ def convertPackage(p): errStr = omc.sendExpression("getErrorString()") if errStr: print(errStr) - raise Exception('echo(false);before:=readFile("%s");\nafter:=readFile("%s");echo(true);\ndiffModelicaFileListings(before, after, OpenModelica.Scripting.DiffFormat.plain, failOnSemanticsChange=true);\ngetErrorString();' % (oldFile, newFile)) + raise Exception('--allowErrorsInDiff is not active:\necho(false);before:=readFile("%s");\nafter:=readFile("%s");echo(true);\ndiffModelicaFileListings(before, after, OpenModelica.Scripting.DiffFormat.plain, failOnSemanticsChange=true);\ngetErrorString();' % (oldFile, newFile)) else: omcAssert(omc, 'writeFile("%s", res)' % (newFile)) isDiff = before != res @@ -157,13 +164,16 @@ def convertPackage(p): diffOutputFile = "converted-libraries/.openmodelica/libraries/%s.diff" % libnameOnFile print("Creating %s" % diffOutputFile) with open(diffOutputFile, "wb") as diffOut: - diffOutput = subprocess.call(["diff", "-ur", os.path.dirname(p), "converted-libraries/.openmodelica/libraries/%s" % libnameOnFile], stdout=diffOut) + diffOutput = subprocess.call(["diff", "-ur", "--exclude", "openmodelica.metadata.json", os.path.dirname(p), "converted-libraries/.openmodelica/libraries/%s" % libnameOnFile], stdout=diffOut) del omc return {"errorsInDiff": errorsInDiff, "path": path, "timeForConversion": timeForConversion, "statsByFile": statsByFile, "gcProfStatsBeforeConversion": gcProfStatsBeforeConversion, "gcProfStatsBefore": gcProfStatsBefore, "gcProfStats": gcProfStats} - + pat = "%s/*/openmodelica.metadata.json" % libdir -with Pool(processes=numThreads) as pool: - res = pool.map(convertPackage, sorted(glob.glob(pat))) +if numThreads==1: + res = [convertPackage(p) for p in sorted(glob.glob(pat))] +else: + with Pool(processes=numThreads) as pool: + res = pool.map(convertPackage, sorted(glob.glob(pat))) for r in res: if r is None: continue diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..5d908a6 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,432 @@ +# The OpenModelicaLibraryTesting result database + +This document describes how the library testing results are stored: the current +sqlite3 layout, what every table and column means, how the scripts query it, and +the PostgreSQL layout the data is being migrated to (issue +[#295](https://github.com/OpenModelica/OpenModelicaLibraryTesting/issues/295)). + +## Where the databases live + +Each test machine keeps a single sqlite3 file called `sqlite3.db` in the working +directory of the test run. The files are published at: + +| machine | URL | on omod-r630-2 | size | tables | rows | +| --- | --- | --- | --- | --- | --- | +| ripper1 | | `/var/www/libraries.openmodelica.org/sqlite3/ripper1/sqlite3.db` | 11.8 GB | 34 | ~69 million | +| ripper2 | | `/var/www/libraries.openmodelica.org/sqlite3/ripper2/sqlite3.db` | 15.5 GB | 54 | ~91 million | + +(sizes as of 2026-08-10; both are at `PRAGMA user_version` 3) + +Every branch/configuration tested on a machine ends up in that machine's file, +which is why the files are large and why two machines cannot test the same job +without overwriting each other's results. The tables are the branches currently +tested (`master`, `newInst-newBackend`, `cpp`, `master-fmi`, `gbode`, `cvode`, +`ida`, `daemode`, ...) plus one per historical release (`v1.9` ... `v1.27`, +`v1.11-fmi` ...). `master` alone holds ~43 million rows on ripper1. + +Six table names exist on both machines - `master`, `newInst`, `heavy_tests`, +`v1.17`, `libversion` and `omcversion` - so a shared database mixes rows from +both. They are distinguished by their `date`, which is unique per run and +machine. + +The scripts always open the file by the hardcoded relative name `sqlite3.db`: +`test.py` writes it, `report.py`, `all-reports.py`, `all-plots.py`, +`single-model.py`, `clean-dates.py` and `clean-empty-omcversion-dates.py` read +it. + +## Schema version + +`test.py` keeps the layout version in sqlite's `PRAGMA user_version` and +migrates on startup (`test.py`, around the `CREATE TABLE` block): + +| user_version | meaning | +| --- | --- | +| 0 | empty/new database; `omcversion` and `libversion` are created | +| 1 | `libversion.confighash` added | +| 2 | `parsing` added to every per-branch table | +| 3 | current layout | + +A database with a higher `user_version` makes `test.py` exit rather than guess. + +## Tables + +### Per-branch result tables + +There is **one table per tested branch/configuration**, named after the branch +(`master`, `gbode`, `ida`, `cvode`, `master-fmi`, `newInst-newBackend`, +`heavy_tests`, ...), created on demand by `test.py`: + +```sql +CREATE TABLE if not exists [] ( + date integer NOT NULL, -- unix epoch: start of the test run + libname text NOT NULL, -- library incl. version suffix, e.g. Buildings_9.1.0 + model text NOT NULL, -- full Modelica class name of the tested model + exectime real NOT NULL, -- wall clock for the whole test of this model [s] + frontend real NOT NULL, -- time in the front end [s] + backend real NOT NULL, -- time in the back end [s] + simcode real NOT NULL, -- time generating SimCode [s] + templates real NOT NULL, -- time running the code generation templates [s] + compile real NOT NULL, -- time compiling the generated code (or building the FMU) [s] + simulate real NOT NULL, -- time simulating [s] + verify real NOT NULL, -- time spent in diffSimulationResults [s] + verifyfail integer NOT NULL, -- number of variables that differ from the reference + verifytotal integer NOT NULL, -- number of variables compared against the reference + finalphase integer NOT NULL, -- how far the model got, see below + parsing real NOT NULL -- time loading/parsing the library [s] +) +``` + +Notes on the values, which are produced by `testmodel.py` and written by +`test.py`: + +- `date` is `int(time.time())` taken **once per test run** (`testRunStartTimeAsEpoch`), + so all rows of one run share the same date. It is the join key to + `omcversion`/`libversion` and the x-axis of every history plot. +- The phase times are exclusive, computed by subtracting the nested OMC timers + from each other (`frontend = frontend - backend`, `backend = backend - simcode`, ...). + A phase that was never reached is stored as `0.0`. +- `compile` is the `build` measurement: `make -f .makefile` for the C + runtime, the FMU build for FMI configurations, and the JIT compile time for + wasm-jit. +- `exectime` is the total wall clock of the model's test process. `test.py` + reads back the most recent value (`SELECT exectime ... ORDER BY date DESC LIMIT 1`) + to sort the queue longest-job-first. +- `verifyfail`/`verifytotal` are `len(diff.vars)` and `diff.numCompared`; a model + without reference variables stores `0`/`0` and still reaches phase 7. + +`finalphase` is the last phase completed; `shared.finalphaseName` maps it to: + +| value | name | meaning | +| --- | --- | --- | +| 0 | Failed | the front end did not finish | +| 1 | FrontEnd | front end ok, back end failed | +| 2 | BackEnd | back end ok, SimCode failed | +| 3 | SimCode | SimCode ok, templates/translation failed | +| 4 | Templates | translated, but compilation/build failed | +| 5 | Compile | built, but the simulation failed | +| 6 | Simulate | simulated, but the result does not verify (or was not compared) | +| 7 | Verify | the result matches the reference file | + +Reports count models per phase with `WHERE finalphase >= i`, so the columns of +the HTML tables are cumulative. + +### `omcversion` + +Maps a test run to the compiler that produced it: + +```sql +CREATE TABLE if not exists [omcversion] ( + date integer NOT NULL, -- same epoch as the result rows of that run + branch text NOT NULL, -- branch/configuration name = result table name + omcversion text NOT NULL -- output of getVersion(), e.g. "OMCompiler v1.26.0-dev.42+g0123abc" +) +``` + +One row per run. `report.py` and `all-reports.py` use it to label a run, and +`all-reports.py` walks it in date order to pair consecutive runs when generating +the regression reports. + +### `libversion` + +Maps a test run to the library versions and configuration used: + +```sql +CREATE TABLE if not exists [libversion] ( + date integer NOT NULL, -- same epoch as the result rows of that run + branch text NOT NULL, + libname text NOT NULL, -- as in the result table + libversion text NOT NULL, -- conf["libraryLastChange"]: version + git revision/zip hash + confighash integer NOT NULL -- hash of the configuration and the reference files +) +``` + +One row per (run, library). `confighash` is `strToHashInt()` over the +configuration dictionary plus the hashes of all reference files, so any change +to the config or to a reference file yields a different value. + +This drives the "do we need to test this at all" decision in `test.py`: before +testing a library it looks for + +```sql +SELECT date,libversion,libname,branch,omcversion FROM [libversion] NATURAL JOIN [omcversion] +WHERE libversion=? AND libname=? AND branch=? AND omcversion=? AND confighash=? ORDER BY date DESC LIMIT 1 +``` + +and skips the library when the exact same combination of library version, OMC +version and configuration was already tested. + +### `datelookup_` (obsolete) + +`datelookup_(date, runDate, libname, branch)` was a cache mapping every +omcversion date to the latest run date of a library. The code that fills it in +`all-plots.py` sits inside a triple-quoted block and is no longer executed; +neither ripper1 nor ripper2 still has such a table. The migration skips them. + +### Indexes + +No index is stored permanently. `test.py` drops `idx__date`, +`idx_omcversion_date` and `idx_libversion_date` on startup (they slow the bulk +insert down), and `report.py`/`all-reports.py`/`all-plots.py` recreate +`idx__date` when they need it. + +## How a test run writes the database + +1. Open `sqlite3.db`, apply the `user_version` migration, `CREATE TABLE IF NOT EXISTS []`. +2. Compute `confighash` per library, skip libraries already covered (query above). +3. Run the tests; each model writes `files/.stat.json`. +4. At the end, in one transaction: one `INSERT` per model into `[]`, one + `INSERT` per library into `[libversion]`, one `INSERT` into `[omcversion]`, + then `conn.commit()`. + +Nothing is written while the tests run, so an aborted run leaves no rows behind, +and two machines running the same job produce two full sets of rows in two +separate files - whichever file is copied back last wins. + +## Housekeeping scripts + +- `clean-dates.py --start --stop`: `DELETE FROM [] WHERE date?` + over every table, then `VACUUM`. Removes a range of bad runs. +- `clean-empty-omcversion-dates.py`: drops `omcversion` rows whose date has no + result rows in the corresponding branch table. + +## PostgreSQL layout + +The PostgreSQL database is a **mirror** of the sqlite3 one: the same tables with +the same names and columns, one table per branch plus `omcversion` and +`libversion`. That way the test scripts can push new results to the network +database with the same statements they use today, and the report scripts need no +query rewriting beyond the sqlite `[name]` / PostgreSQL `"name"` quoting. + +Only the types are adapted: + +| sqlite3 | PostgreSQL | +| --- | --- | +| `integer` | `bigint` (`integer` for `verifyfail`, `verifytotal`, `finalphase`) | +| `real` | `double precision` | +| `text` | `text` | +| `NOT NULL` on every column | only on the key columns, see below | +| `PRAGMA user_version` | not used; the `parsing` column always exists | +| `datelookup_*` | not migrated (derived data, no longer generated) | + +### Keys + +Each table gets a unique key, which sqlite3 never had: + +| table | key | +| --- | --- | +| `` | `(date, libname, model)` - a run tests every model of a library once | +| `omcversion` | `(date, branch)` - one row per run | +| `libversion` | `(date, branch, libname, confighash)` - one row per run and library | + +This is what makes a shared database possible: results from a second test +machine can be merged into a table that already holds another machine's rows, +and a run that is pushed twice cannot produce duplicates. + +Only those key columns are `NOT NULL`. The sqlite3 tables declare every column +`NOT NULL`, but `CREATE TABLE if not exists` means tables created by an older +`test.py` keep their old, laxer declaration, so the historical data does not +necessarily hold up. `libversion.libversion` for instance stores empty strings +for some old runs. + +So a branch table becomes: + +```sql +CREATE TABLE "master" ( + date bigint NOT NULL, + libname text NOT NULL, + model text NOT NULL, + exectime double precision NOT NULL, + frontend double precision NOT NULL, + backend double precision NOT NULL, + simcode double precision NOT NULL, + templates double precision NOT NULL, + compile double precision NOT NULL, + simulate double precision NOT NULL, + verify double precision NOT NULL, + verifyfail integer NOT NULL, + verifytotal integer NOT NULL, + finalphase integer NOT NULL, + parsing double precision NOT NULL +); +``` + +Two things to keep in mind when querying it: + +- Identifiers must be **double quoted**, not bracketed: branch names such as + `newInst-newBackend` contain upper case letters and dashes, which PostgreSQL + would otherwise fold to lower case or reject. +- All test machines write into the same tables, so a run is identified by + `date` (plus `branch`) exactly as before. Use `--pgschema ripper1` if a + machine should be mirrored into a schema of its own instead. + +Indexes are created by `sqlite2postgres.py --index` rather than on the fly: +`(date)` on every table, `(branch, date)` on `omcversion`, +`(branch, libname, date)` on `libversion` and `(libname, date)` on the branch +tables. + +## Using the shared database + +Every script takes `--db`, which is a path to a local sqlite3 file (the default, +`sqlite3.db`) or a `postgresql://` URL: + +```bash +./test.py --branch=master --db=postgresql://om@openmodelica.org/omdb configs/conf.json +./report.py --branches=master --db=postgresql://om@openmodelica.org/omdb configs/conf.json +``` + +The password comes from `PGPASSWORD` or `~/.pgpass`, never from the URL or the +command line. `resultsdb.py` holds the two backends behind one interface; the +scripts write the same statements for both, with `?` as the placeholder, and ask +the connection where the dialects genuinely differ (`quote()`, `tableExists()`, +`groupConcat()`, `countIf()`, `likeNoCase()`, `insertIgnore()`). + +### Job claiming + +The point of the shared database is that two machines can test at the same time +without overwriting each other. Before testing a library, `test.py` claims the +job in + +```sql +CREATE TABLE job_claim ( + branch text, libname text, libversion text, omcversion text, confighash bigint, + host text NOT NULL, state text NOT NULL, + claimed_at timestamptz NOT NULL DEFAULT now(), + heartbeat timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (branch, libname, libversion, omcversion, confighash) +); +``` + +The key is exactly the question "which library, in which version, against which +compiler and configuration": the same combination the run already uses to decide +whether results exist. A claim is taken with `INSERT ... ON CONFLICT DO UPDATE +... WHERE` so that only one machine can win it, and a machine that loses prints + +``` +Skipping Buildings_9.1.0 as ripper2 has been testing it since 2026-08-10 22:14:03 +``` + +and moves on to the next library instead of repeating the work. The winner +refreshes `heartbeat` every minute from a background thread and sets +`state='done'` when the results are written. A machine that dies stops sending +its heartbeat, and after 30 minutes (`STALE_CLAIM_MINUTES`) another machine may +take its jobs over, so a crash does not park a library forever. + +Nothing of this applies to a local sqlite3 file: it has a single writer, and +`claim()` always says yes. + +### In Jenkins + +The pipeline has a `postgres` parameter, on by default, and sets two variables +for every stage: + +```groovy +environment { + LIBTEST_DB = "${params.postgres ? 'postgresql://om@openmodelica.org/omdb' : 'sqlite3.db'}" + PGPASSFILE = credentials('omdb-pgpass') +} +``` + +`LIBTEST_DB` is where `--db` defaults to, so no invocation has to spell it out, +and `omdb-pgpass` is a Jenkins *secret file* credential holding a single line: + +``` +openmodelica.org:5432:omdb:om: +``` + +libpq reads the password from that file, so it never appears on a command line +or in the build log. `resultsdb.py` takes a private copy of the file when its +permissions let anyone else read it, because libpq silently ignores such a file +and then fails with `fe_sendauth: no password supplied`. + +With `postgres` ticked, a job no longer downloads the machine's `sqlite3.db` +before the run nor publishes it back afterwards - the step that made two +machines overwrite each other. Untick the parameter and the old behaviour is +back, unchanged. + +The test machines need `psycopg2`: `pip3 install psycopg2-binary`, or a rebuild +of the images, since it is in `requirements.txt` and in `.CI/build-dep`. + +## Migrating + +Run this **on omod-r630-2 (openmodelica.org)**: the sqlite3 files and the +PostgreSQL server are on the same machine, so the data never goes over the +network. Pushing it from a developer machine works too, but a home uplink does +0.3-1.8 MB/s, which means hours for ~27 GB. + +```bash +export PGPASSFILE=~/.pgpass # never put the password on the command line +DB=/var/www/libraries.openmodelica.org/sqlite3 +./sqlite2postgres.py --host localhost --sqlite $DB/ripper1/sqlite3.db --source ripper1 +./sqlite2postgres.py --host localhost --index +./sqlite2postgres.py --host localhost --sqlite $DB/ripper2/sqlite3.db --source ripper2 --skip-existing +./sqlite2postgres.py --host localhost --index +./sqlite2postgres.py --host localhost --sqlite $DB/ripper1/sqlite3.db --verify +``` + +The order matters. ripper1 goes in first and without an index, which is the +fast path. `--index` then creates the unique keys, so that ripper2, loaded with +`--skip-existing`, keeps whatever is already there whenever a key collides - +ripper1 wins. The second `--index` covers the tables that only exist on ripper2. + +On the databases as of 2026-08-10 the priority never actually fires: not one +`(branch, date)` is shared between the two machines, not even for the four +branches both of them test, so the merge is a plain union. The rule matters for +re-runs and for two machines pushing results later on. + +### Disk space + +A branch table row measures **212 bytes** in PostgreSQL (measured on `v1.10`, +average `model` length 57, `libname` 17). The whole migration is therefore about + +- 34 GB of table data for the ~160 million rows, plus +- 10-12 GB for the `(date)` and `(libname, date)` indexes. + +so **plan for ~50 GB**. On omod-r630-2 the cluster lives in +`/var/lib/postgresql/16` on the root LV, which has 14 GB free, while the `data` +ZFS pool has 2.9 TB free. Put the database on the pool before loading, e.g. + +```bash +sudo zfs create -o mountpoint=/data/postgres -o compression=lz4 -o recordsize=16k data/postgres +sudo install -d -o postgres -g postgres /data/postgres/omdb +sudo -u postgres psql -c "CREATE TABLESPACE omdb_ts LOCATION '/data/postgres/omdb'" +sudo -u postgres psql -c "ALTER DATABASE omdb SET TABLESPACE omdb_ts" # needs no open connections +``` + +`lz4` on the dataset typically cuts this data to well under half, since the +model names repeat in every run. + +### Catching up with the jobs that are still running + +The migration is a snapshot: a test run that was started before it, or any run +that still writes its own sqlite3 file, adds rows the shared database has never +seen. `--catch-up` copies them over: + +```bash +./sqlite2postgres.py --host localhost --sqlite $DB/ripper1/sqlite3.db --source ripper1 --catch-up +./sqlite2postgres.py --host localhost --sqlite $DB/ripper2/sqlite3.db --source ripper2 --catch-up +``` + +It reads the database from the start and keeps the runs the shared database +does not have, which takes two to three minutes per machine. Not "everything +past the rowid the migration stopped at", tempting as that is: `VACUUM` +renumbers the rowids of these tables and `clean-empty-omcversion-dates.py` runs +one after every test, so that number does not survive a test run. Picking the +runs by date is also what makes it correct for `master`, `newInst`, +`heavy_tests` and `v1.17`, where both machines write into the same table. + +Repeating it costs nothing but the reading: the keys reject anything already +there. Run it once more right after the jobs are switched to the shared +database; from then on nothing writes the sqlite3 files any more and there is +nothing left to catch up with. + +`--source` only names the machine for the bookkeeping table +`migration_progress(source, tbl, last_rowid, rows_read, done)`, which records how +far each sqlite table has been copied. Each batch is committed together with its +progress row, so an interrupted migration continues from the last rowid that +made it in and never copies a batch twice; the command can simply be run again. +Rows are streamed in batches of `--batch` (200000 by default) through +`COPY ... FROM STDIN`, so memory use does not depend on the size of the database. + +`COPY` is used in its text format rather than CSV on purpose: an empty CSV field +reads back as NULL, which would silently turn the empty `libversion` strings in +the old data into NULLs. diff --git a/drop-pr-tables.py b/drop-pr-tables.py new file mode 100755 index 0000000..73e7c8d --- /dev/null +++ b/drop-pr-tables.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""Drop the result tables of pull requests that have been dealt with. + +Testing a pull request fills a pr- table like any other branch, about 19500 +rows and a few megabytes, and unlike a branch it is never written to again once +the pull request is merged or closed. This drops those tables, and the rows the +runs left in the other tables, for pull requests that are no longer open or that +were tested long enough ago not to matter. + +Nothing is dropped without --yes; without it the tables are only listed. +""" + +import argparse, datetime, json, os, re, time, urllib.error, urllib.request +import resultsdb + +parser = argparse.ArgumentParser(description='OpenModelica library testing pull request cleanup') +parser.add_argument('--repo', default="OpenModelica/OpenModelica", help='the repository the pull requests belong to') +parser.add_argument('--older-than', type=int, default=60, metavar='DAYS', + help='also drop a pull request still open whose newest run is older than this (0: never)') +parser.add_argument('--yes', action='store_true', help='actually drop them') +resultsdb.addArgument(parser) +args = parser.parse_args() + +prTableRe = re.compile(r"^pr-([0-9]+)$") + +db = resultsdb.connect(args.db) +cursor = db.cursor() + + +def pullRequestState(number): + """"open", "closed", "merged", or why we could not tell.""" + url = "https://api.github.com/repos/%s/pulls/%s" % (args.repo, number) + request = urllib.request.Request(url, headers={"Accept": "application/vnd.github+json"}) + # The rate limit is 60 requests an hour without one, which is enough for the + # handful of tables this looks at, but a token raises it and costs nothing. + token = os.environ.get("GITHUB_TOKEN") + if token: + request.add_header("Authorization", "Bearer %s" % token) + try: + pr = json.loads(urllib.request.urlopen(request).read().decode("utf-8")) + except urllib.error.HTTPError as e: + return "no answer from GitHub (%s)" % e + except Exception as e: + return "no answer from GitHub (%s)" % e + if pr.get("merged_at"): + return "merged" + return pr.get("state") or "unknown" + + +def newestRun(table): + (date,) = cursor.execute("SELECT max(date) FROM %s" % db.quote(table)).fetchone() + return date + + +def drop(table, branch): + """The table of a run, and everything the run wrote about itself elsewhere.""" + cursor.execute("DROP TABLE %s" % db.quote(table)) + for other in ["omcversion", "libversion", "history", "job_claim"]: + if db.tableExists(other): + cursor.execute("DELETE FROM %s WHERE branch=?" % db.quote(other), (branch,)) + db.commit() + + +tables = sorted((t for t in db.tables() if prTableRe.match(t)), + key=lambda t: int(prTableRe.match(t).group(1))) +if not tables: + raise SystemExit("No pull request tables in this database") + +dropping = [] +for table in tables: + number = prTableRe.match(table).group(1) + date = newestRun(table) + age = (time.time() - date) / 86400.0 if date else 0 + state = pullRequestState(number) + stale = args.older_than and date and age > args.older_than + why = None + if state in ("merged", "closed"): + why = "%s, tested %d days ago" % (state, age) + elif stale: + why = "still %s, but tested %d days ago" % (state, age) + print("%-12s %-8s %s%s" % (table, state, + "last run %s" % datetime.datetime.fromtimestamp(date) if date else "no runs", + ", dropping: %s" % why if why else "")) + if why: + dropping.append((table, why)) + +if not dropping: + raise SystemExit(0) +if not args.yes: + print("\n%d tables would be dropped; pass --yes to drop them" % len(dropping)) + raise SystemExit(0) +for (table, why) in dropping: + print("Dropping %s (%s)" % (table, why)) + drop(table, table) +print("The reports and files published for them are not touched; they are on the web server.") diff --git a/history.html.tpl b/history.html.tpl index 58d7a79..5647cf3 100644 --- a/history.html.tpl +++ b/history.html.tpl @@ -1,5 +1,5 @@ - + OpenModelica #BRANCH# from #DATE1# to #DATE2# @@ -26,10 +26,15 @@

OpenModelica Changes

- + #OMCGITLOG#
CommitAuthorSummary
CommitDateAuthorSummary
#3rdParty# +

OpenModelicaLibraryTesting Changes

+ + +#OMCLIBRARYTESTINGGITLOG# +
CommitDateAuthorSummary

Library Changes

diff --git a/library.html.tpl b/library.html.tpl index 9eb1f86..2cc608e 100644 --- a/library.html.tpl +++ b/library.html.tpl @@ -1,5 +1,5 @@ - +#fileName# test using OpenModelica @@ -29,24 +29,30 @@
-

Total time taken: #totalTime#

-

System info: #sysInfo#

-

OpenModelica Version: #omcVersion#

+

+Test started: #timeStart#
+Total time taken: #totalTime#
+System info: #sysInfo#

+

OpenModelica Version: #omcVersion#
+#fmiToolVersion# #fmi# -

Test started: #timeStart#

-

Tested Library: #libraryVersionRevision#

+OpenModelicaLibraryTesting Changes
+#OpenModelicaLibraryTesting#

+

Tested Library: #fileName# #libraryVersionRevision#

 #metadata#

-

BuildModel time limit: #ulimitOmc#s

-

Simulation time limit: #ulimitExe#s

-

Default tolerance: #default_tolerance#

-

Optimization level: #optlevel#

+

+BuildModel time limit: #ulimitOmc#s
+Simulation time limit: #ulimitExe#s
+Default tolerance: #defaultTolerance#
+Default number of intervals: #defaultNumberOfIntervals#
+Optimization level: #optlevel#

#referenceFiles# #referenceTool# Flags:
#customCommands#
Config:
#config#

Links are provided if getErrorString() or the simulation generates output. The links are coded with red if there were errors, yellow if there were warnings, and normal links if there are only notifications.

- + #testsHTML#
ModelVerifiedSimulateTotal buildModelParsingFrontendBackendSimCodeTemplatesCompile
ModelVerifiedSimulateTotal buildModelParsingFrontendBackendSimCodeTemplatesCompileTotal Execution
diff --git a/overview.html.tpl b/overview.html.tpl index e58f616..b62305c 100644 --- a/overview.html.tpl +++ b/overview.html.tpl @@ -1,4 +1,5 @@ - + + #title# + + +

OpenModelica pull request #PR# against #BASELINE#

+ +#CAVEATS# +

The baseline is the newest run of #BASELINE#, not the commit the pull request is +based on, so a difference can also come from something merged into #BASELINE# since +the pull request was branched.

+ +

The two runs

+ + + + + +
BranchRunCompilerMachine
Baseline#BASELINE##DATE1##OMCVERSION1##HOST1#
Pull request#BRANCH##DATE2##OMCVERSION2##HOST2#
+ +

Summary

+ + + + + + + + + +
Models compared#NUMCOMPARED#
Number of Improvements#NUMIMPROVE#
Number of Regressions#NUMREGRESSION#
Number of Performance Improvements#NUMPERFIMPROVE#
Number of Performance Regressions#NUMPERFREGRESSION#
Models only in the pull request run#NUMONLYPR#
Models only in the baseline run#NUMONLYBASELINE#
+ +

Library Changes

+ + +#LIBCHANGES# +
LibraryChange
+ +

Models Affected

+ + +#MODELCHANGES# +
LibraryModelChange
+ +

Models Only in One of the Runs

+ + +#MODELSONLYINONE# +
LibraryModelWhere
+ + + diff --git a/report.py b/report.py index ddc6240..97cbd80 100755 --- a/report.py +++ b/report.py @@ -3,11 +3,12 @@ import sys, argparse import simplejson as json -import shared +import shared, resultsdb parser = argparse.ArgumentParser(description='OpenModelica library testing report generation tool') parser.add_argument('configs', nargs='*') -parser.add_argument('--branches', default='') +parser.add_argument('--branches', default='master') +resultsdb.addArgument(parser) args = parser.parse_args() configs = args.configs @@ -19,11 +20,11 @@ dates = {} dates_str = {} fields = ["exectime", "parsing", "frontend", "backend", "simcode", "templates", "compile", "simulate", "verify"] -entryhead = "BranchTotalParsingFrontendBackendSimCodeTemplatesCompilationSimulationVerification\n" +entryhead = "BranchTotalParsingFrontendBackendSimCodeTemplatesCompilationSimulationVerification\n" libs = {} -import cgi, sqlite3, time, datetime +import html, time, datetime from omcommon import friendlyStr, multiple_replace configs_lst = [shared.readConfig(c) for c in configs] @@ -32,29 +33,44 @@ configs = configs + c libnames = set(shared.libname(library,conf) for (library,conf) in configs) -conn = sqlite3.connect('sqlite3.db') -cursor = conn.cursor() +db = resultsdb.connect(args.db) +cursor = db.cursor() nmodels = {} nsimulate = {} exectime = {} +missing_branches = [] for branch in branches: - cursor.execute("SELECT date FROM [%s] ORDER BY date DESC LIMIT 1" % branch) - v = cursor.fetchone()[0] + try: + cursor.execute("SELECT date FROM %s ORDER BY date DESC LIMIT 1" % db.quote(branch)) + one = cursor.fetchone() + if one == None: + print("No such table '%s'; specify it using --branch=XXX when running test.py" % branch) + # ignore this table and continue + missing_branches.append(branch) + continue + else: + v = one[0] + except: + print("No such table '%s'; specify it using --branch=XXX when running test.py" % branch) + # ignore this table and continue + missing_branches.append(branch) + continue + dates_str[branch] = str(datetime.datetime.fromtimestamp(v).strftime('%Y-%m-%d %H:%M:%S')) - cursor.execute('''CREATE INDEX IF NOT EXISTS [idx_%s_date] ON [%s](date)''' % (branch,branch)) + db.createDateIndex(branch) dates[branch] = {} branch_nmodels = 0 for libname in libnames: - cursor.execute("SELECT date FROM [%s] WHERE libname=? ORDER BY date DESC LIMIT 1" % branch, (libname,)) + cursor.execute("SELECT date FROM %s WHERE libname=? ORDER BY date DESC LIMIT 1" % db.quote(branch), (libname,)) v = cursor.fetchone() if v is None: dates[branch][libname] = 0 continue dates[branch][libname] = v[0] - for x in cursor.execute("SELECT model FROM [%s] WHERE libname=? AND date=?" % branch, (libname,v[0])): + for x in cursor.execute("SELECT model FROM %s WHERE libname=? AND date=?" % db.quote(branch), (libname,v[0])): if libname not in libs: libs[libname] = set() libs[libname].add(x[0]) @@ -64,14 +80,17 @@ exectime[branch] = 0.0 entries = "" +# removing missing branches +for branch in missing_branches: + branches.remove(branch) def checkEqual(iterator): return len(set(iterator)) <= 1 for lib in sorted(libs.keys()): models = libs[lib] - entries += "

%s

\n" % lib - branches_versions = [(cursor.execute("SELECT libversion FROM [libversion] WHERE libname=? AND branch=? ORDER BY date DESC LIMIT 1", (lib,branch)).fetchone() or ["unknown"])[0] for branch in branches] + entries += "

%s

\n" % lib + branches_versions = [(cursor.execute("SELECT libversion FROM libversion WHERE libname=? AND branch=? ORDER BY date DESC LIMIT 1", (lib,branch)).fetchone() or ["unknown"])[0] for branch in branches] all_equal = checkEqual(branches_versions) if not all_equal: entries += "\n" @@ -91,12 +110,12 @@ def checkEqual(iterator): master_models = [] for i in range(0,8): i_models = set() - for v in cursor.execute("SELECT model FROM [%s] WHERE date=? AND finalphase>=? AND libname=?" % (branch), (dates[branch][lib],i,lib)): + for v in cursor.execute("SELECT model FROM %s WHERE date=? AND finalphase>=? AND libname=?" % (db.quote(branch)), (dates[branch][lib],i,lib)): i_models.add(v[0]) master_models.append(i_models) models[branch] = master_models for branch in branches: - vs = [cursor.execute("SELECT COUNT(*) FROM [%s] WHERE date=? AND finalphase>=? AND libname=?" % (branch), (dates[branch][lib],i,lib)).fetchone()[0] for i in range(0,8)] + vs = [cursor.execute("SELECT COUNT(*) FROM %s WHERE date=? AND finalphase>=? AND libname=?" % (db.quote(branch)), (dates[branch][lib],i,lib)).fetchone()[0] for i in range(0,8)] warnings = [] entries += '' % (branch,lib,lib,branch) for i in [0]+list(range(0,len(vs))): @@ -104,11 +123,11 @@ def checkEqual(iterator): diff2 = models[branches[-1]][i] - models[branch][i] diff_text = "" if len(diff2)>0: - diff_text += "

Now working in %s, failed in %s:
\n" % (branches[-1],branch) + "
\n".join(sorted(diff2)) + "

" + diff_text += "

Now working in %s, failed in %s:
\n" % (branches[-1],branch) + "
\n".join(sorted(diff2)) + "

" if len(diff1)>0: - diff_text += "

Now failing in %s, worked in %s:
\n" % (branches[-1],branch) + "
\n".join(sorted(diff1)) + "

" + diff_text += "

Now failing in %s, worked in %s:
\n" % (branches[-1],branch) + "
\n".join(sorted(diff1)) + "

" if False and diff_text: - diff_text += "

Debug:
\n" + "
\n".join(models[branch][i]) + "

" + diff_text += "

Debug:
\n" + "
\n".join(models[branch][i]) + "

" entries += '%s%s' % (' class="warning"' if old_vs and old_vs[i]>vs[i] else (' class="better"' if old_vs and old_vs[i]%s' % diff_text) if diff_text else "") entries += "" nsimulate[branch] += vs[6] @@ -120,8 +139,8 @@ def checkEqual(iterator): entries += "
%s
\n" entries += entryhead for branch in branches: - vs = [cursor.execute("SELECT COUNT(*) FROM [%s] WHERE date=? AND finalphase>=? AND libname=?" % (branch), (dates[branch][lib],i,lib)).fetchone()[0] for i in range(0,8)] - sums = [cursor.execute("SELECT SUM(%s) FROM [%s] WHERE date=? AND libname=?" % (fields[i],branch), (dates[branch][lib],lib)).fetchone()[0] or 0 for i in range(0,9)] + vs = [cursor.execute("SELECT COUNT(*) FROM %s WHERE date=? AND finalphase>=? AND libname=?" % (db.quote(branch)), (dates[branch][lib],i,lib)).fetchone()[0] for i in range(0,8)] + sums = [cursor.execute("SELECT SUM(%s) FROM %s WHERE date=? AND libname=?" % (fields[i],db.quote(branch)), (dates[branch][lib],lib)).fetchone()[0] or 0 for i in range(0,9)] entries += '' % (branch,lib,lib,branch) entries += ("\n" % (friendlyStr(sums[0]),friendlyStr(sums[1]),friendlyStr(sums[2]),friendlyStr(sums[3]),friendlyStr(sums[4]),friendlyStr(sums[5]),friendlyStr(sums[6]),friendlyStr(sums[7]),friendlyStr(sums[8]))) exectime[branch] += sums[0] @@ -129,9 +148,9 @@ def checkEqual(iterator): # print(sorted(list(libs[lib]))) nummodels = sum(len(l) for l in libs.values()) -branches_lines = [("%d\n" % (cgi.escape(branch), cgi.escape( - (cursor.execute("SELECT omcversion FROM [omcversion] WHERE date=? AND branch=?", (max(dates[branch][lib] for lib in libnames),branch)).fetchone() or ["unknown"])[0] - ), cgi.escape(dates_str[branch]), friendlyStr(exectime[branch]), +branches_lines = [("%d\n" % (html.escape(branch), html.escape( + (cursor.execute("SELECT omcversion FROM omcversion WHERE date=? AND branch=?", (max(dates[branch][lib] for lib in libnames),branch)).fetchone() or ["unknown"])[0] + ), html.escape(dates_str[branch]), friendlyStr(exectime[branch]), " class=\"warning\"" if nummodels!=nmodels[branch] else "", nsimulate[branch], nmodels[branch] diff --git a/requirements.txt b/requirements.txt index 59c262a..c24fbe9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,9 @@ datetime +joblib matplotlib monotonic natsort -joblib -ompython -simplejson +ompython==3.6 psutil +psycopg2-binary +simplejson diff --git a/resultsdb.py b/resultsdb.py new file mode 100644 index 0000000..92773ab --- /dev/null +++ b/resultsdb.py @@ -0,0 +1,546 @@ +#!/usr/bin/env python3 +""" +The result database of the library testing, either local or shared. + +Historically every test machine wrote its results to its own sqlite3 file, +copied in before a run and copied back afterwards, so two machines running the +same job overwrote each other. The scripts can now talk to a shared PostgreSQL +database instead, where the machines coordinate through [job_claim] and nobody +overwrites anybody. + + db = connect("sqlite3.db") # the old local file + db = connect("postgresql://om@openmodelica.org/omdb") # the shared database + +Both backends take the same statements, with "?" as the placeholder. Where the +two dialects genuinely differ - quoting a branch name, testing whether a table +exists, concatenating a group, counting a condition - ask the connection +instead of writing it out, see quote()/tableExists()/groupConcat()/countIf(). + +PostgreSQL needs psycopg2 (pip install psycopg2-binary) and reads the password +from PGPASSWORD or ~/.pgpass, never from the URL. +""" + +import os +import re +import socket +import sqlite3 +import threading +import time + +# A claim older than this without a heartbeat belongs to a machine that died, +# and another machine may take the job over. +STALE_CLAIM_MINUTES = 30 +HEARTBEAT_SECONDS = 60 + +# The columns of a per-branch result table, with the type spelled per backend. +BRANCH_COLUMNS = [ + ("date", "bigint"), ("libname", "text"), ("model", "text"), ("exectime", "real"), + ("frontend", "real"), ("backend", "real"), ("simcode", "real"), ("templates", "real"), + ("compile", "real"), ("simulate", "real"), ("verify", "real"), + ("verifyfail", "int"), ("verifytotal", "int"), ("finalphase", "int"), ("parsing", "real"), +] +SQLITE_TYPES = {"bigint": "integer", "int": "integer", "real": "real", "text": "text"} +POSTGRES_TYPES = {"bigint": "bigint", "int": "integer", "real": "double precision", "text": "text"} + +# What identifies a row, so that two machines writing the same shared table +# cannot store the same result twice. Mirrors sqlite2postgres.py. +KEYS = { + "omcversion": ["date", "branch"], + "libversion": ["date", "branch", "libname", "confighash"], +} +BRANCH_KEY = ["date", "libname", "model"] + +# What a claim is about: this machine is testing this library of this branch. +# Not the configuration it is testing it with - the configuration hash covers +# the reference files, and two machines that fetched them either side of an +# upstream commit would then both claim the same library and both test it, +# which is the duplication the claim exists to prevent. The versions are kept +# to say who is testing what, and the run still decides for itself whether it +# already has results for an exact configuration. +JOB_CLAIM_KEY = ("branch", "libname") + +JOB_CLAIM = """ +CREATE TABLE IF NOT EXISTS job_claim ( + branch text NOT NULL, + libname text NOT NULL, + libversion text, + omcversion text, + confighash bigint, + host text NOT NULL, + state text NOT NULL, + claimed_at timestamptz NOT NULL DEFAULT now(), + heartbeat timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (branch, libname) +) +""" + + +# Jenkins sets LIBTEST_DB once for the whole pipeline rather than passing --db +# to every script invocation. +DEFAULT_DB = os.environ.get("LIBTEST_DB") or "sqlite3.db" +DB_HELP = ("Result database: a local sqlite3 file, or a postgresql://user@host/database URL for " + "the shared one, taken from the LIBTEST_DB environment variable when not given. " + "Several machines can write to the shared database at the same time; a library " + "another machine is already testing is skipped instead of tested twice. The password " + "is read from PGPASSWORD or ~/.pgpass, never from the URL.") + + +def addArgument(parser): + """Give a script the --db option, spelled the same way everywhere.""" + parser.add_argument("--db", default=DEFAULT_DB, help=DB_HELP) + + +def _fixPgpassPermissions(): + """libpq ignores a password file others can read, and then fails to connect. + + Jenkins hands the credential to the build as a file it created itself, so + rather than have every job remember to chmod it, take a private copy. + """ + path = os.environ.get("PGPASSFILE") + if not path or not os.path.isfile(path): + return + if not (os.stat(path).st_mode & 0o077): + return + import shutil, tempfile + fd, private = tempfile.mkstemp(prefix="pgpass.") + os.close(fd) + os.chmod(private, 0o600) + shutil.copyfile(path, private) + os.environ["PGPASSFILE"] = private + + +def connect(url): + """Open the result database named by url: a path or a postgresql:// URL.""" + if url.startswith("postgres://") or url.startswith("postgresql://"): + return _Postgres(url) + return _Sqlite(url) + + +class _Db: + """What the testing and report scripts use; the backends fill in the rest.""" + + def cursor(self): + return _Cursor(self, self.conn.cursor()) + + def execute(self, sql, params=()): + return self.cursor().execute(sql, params) + + def commit(self): + self.conn.commit() + + def close(self): + self.conn.close() + + # The errors that mean the connection is gone rather than the statement bad. + lostConnection = () + + def record(self, statement, params): + """Remember a statement in case the transaction has to be replayed.""" + + def recover(self): + """Reconnect after the connection was lost and replay the transaction.""" + raise NotImplementedError + + def insertIgnore(self): + """The clause that makes an INSERT skip a row that is already there.""" + return "" + + def createDateIndex(self, branch): + """The index the report queries need; test.py drops it before a run.""" + self.execute("CREATE INDEX IF NOT EXISTS %s ON %s (date)" + % (self.quote("idx_%s_date" % branch), self.quote(branch))) + + def claim(self, branch, libname, libversion, omcversion, confighash): + """True when this machine may test that library, False when another one is. + + Only the shared database can say no; a local file has a single writer. + """ + return True + + def claimedBy(self, branch, libname, libversion, omcversion, confighash): + return ("this machine", None) + + def release(self): + """Mark the claims of this run as finished.""" + + def columns(self, table): + """The columns a table has, for the ones an older database may not have.""" + raise NotImplementedError + + def vacuum(self): + self.conn.execute("VACUUM") + + +class _Cursor: + """A cursor that takes "?" placeholders whatever the backend wants.""" + + def __init__(self, db, cursor): + self.db = db + self.cursor = cursor + + def execute(self, sql, params=()): + # psycopg2 only looks for placeholders when parameters are passed, so a + # statement without any must not be handed an empty tuple. + params = tuple(params) + statement = self.db.sql(sql, bool(params)) + try: + self.cursor.execute(statement, self.db.params(params)) + except self.db.lostConnection as e: + # The results of a run are written in one transaction at the end of it, + # after the connection has been idle for hours, which is exactly when a + # firewall or a restarted server has dropped it. Reconnecting and + # replaying what the transaction had so far costs nothing and saves the + # whole run; the keys make replaying it harmless. + print("Lost the connection to the database (%s); reconnecting" % str(e).strip()) + self.cursor = self.db.recover() + self.cursor.execute(statement, self.db.params(params)) + self.db.record(statement, self.db.params(params)) + return self + + def fetchone(self): + return self.cursor.fetchone() + + def fetchall(self): + return self.cursor.fetchall() + + def __iter__(self): + return iter(self.cursor) + + +class _Sqlite(_Db): + """The per-machine sqlite3 file the testing has always used.""" + + name = "sqlite3" + + def __init__(self, path): + self.conn = sqlite3.connect(path) + self.path = path + + def sql(self, sql, hasParams=False): + return sql + + def params(self, params): + return params + + def quote(self, ident): + """Quote a table or column name, typically a branch name.""" + return "[%s]" % ident + + def createTables(self, branch): + """The schema migration test.py has always done, plus the branch table.""" + cursor = self.cursor() + user_version = self.userVersion() + if user_version == 0: + # Table to lookup from a run (date, branch) to omcversion used + cursor.execute("CREATE TABLE if not exists [omcversion] (date integer NOT NULL, branch text NOT NULL, omcversion text NOT NULL)") + # Table to lookup from a run (date, branch) which library versions were used, + # and the machine that produced them + cursor.execute("CREATE TABLE if not exists [libversion] (date integer NOT NULL, branch text NOT NULL, libname text NOT NULL, libversion text NOT NULL, confighash integer NOT NULL, host text, sysinfo text)") + elif user_version == 1: + cursor.execute("ALTER TABLE [libversion] ADD COLUMN confighash integer NOT NULL DEFAULT(0)") + self.addLibversionHost(cursor) + elif user_version == 2: + for tbl in [t for t in self.tables() if t not in ["libversion", "omcversion"]]: + cursor.execute("ALTER TABLE [%s] ADD COLUMN parsing real NOT NULL DEFAULT(0.0)" % tbl) + self.addLibversionHost(cursor) + elif user_version == 3: + self.addLibversionHost(cursor) + elif user_version != 4: + raise SystemExit("Unknown schema user_version=%d" % user_version) + + cols = ", ".join("%s %s NOT NULL" % (c, SQLITE_TYPES[t]) for c, t in BRANCH_COLUMNS) + cursor.execute("CREATE TABLE if not exists %s (%s)" % (self.quote(branch), cols)) + # The indexes only slow the run's inserts down; the report scripts add them back. + cursor.execute("DROP INDEX IF EXISTS [idx_%s_date]" % branch) + cursor.execute("DROP INDEX IF EXISTS idx_omcversion_date") + cursor.execute("DROP INDEX IF EXISTS idx_libversion_date") + self.setUserVersion(4) + + def addLibversionHost(self, cursor): + """Add the host columns to an existing [libversion]. + + ADD COLUMN only, so the rows already there keep their results and simply + read back NULL for a run whose machine was never recorded. + """ + have = set(r[1] for r in cursor.execute("PRAGMA table_info([libversion])")) + for col in ["host", "sysinfo"]: + if col not in have: + cursor.execute("ALTER TABLE [libversion] ADD COLUMN %s text" % col) + + def tables(self): + return [t for (t,) in self.conn.execute("SELECT name FROM sqlite_master WHERE type='table'")] + + def columns(self, table): + return [r[1] for r in self.conn.execute("PRAGMA table_info(%s)" % self.quote(table))] + + def tableExists(self, name): + return self.conn.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (name,)).fetchone() is not None + + def groupConcat(self, expr, orderBy=None): + # sqlite keeps the order of the subquery that feeds the aggregate. + return "GROUP_CONCAT(%s)" % expr + + def countIf(self, cond): + return "COUNT(%s or null)" % cond + + def likeNoCase(self, column): + """A case insensitive comparison against a "?" placeholder.""" + return "%s LIKE ? COLLATE NOCASE" % column + + def userVersion(self): + return self.conn.execute("PRAGMA user_version").fetchone()[0] + + def setUserVersion(self, v): + self.conn.execute("PRAGMA user_version=%d" % v) + + +class _Postgres(_Db): + """The shared database several test machines write to at the same time.""" + + name = "postgresql" + + def __init__(self, url): + try: + import psycopg2 + except ImportError: + raise SystemExit("PostgreSQL support needs psycopg2: pip install psycopg2-binary") + # The password belongs in PGPASSWORD or ~/.pgpass, not in the URL. + _fixPgpassPermissions() + self.url = url + self.lostConnection = (psycopg2.OperationalError, psycopg2.InterfaceError) + self.pending = [] + self.conn = self._connect() + self.host = socket.gethostname() + self.claims = [] + self.heartbeatThread = None + self.execute(JOB_CLAIM) + self._migrateJobClaim() + self.commit() + + def _connect(self): + """Open the connection, asking the kernel to keep it alive. + + A run holds this connection open while it tests, which is hours during + which nothing is sent on it, and an idle connection is what a firewall or + a NAT quietly drops. The keepalives make that visible rather than fatal. + """ + import psycopg2 + conn = psycopg2.connect(self.url, keepalives=1, keepalives_idle=60, + keepalives_interval=10, keepalives_count=5) + conn.autocommit = False + return conn + + # Only what changes the database has to be replayed; a run also makes tens of + # thousands of queries, which would fill the buffer for nothing. + WRITES = ("insert", "update", "delete", "create", "drop", "alter") + + def record(self, statement, params): + first = statement.lstrip().split(None, 1) + if first and first[0].lower() in self.WRITES: + self.pending.append((statement, params)) + + def recover(self): + """Reconnect and replay the transaction that the lost connection took. + + Everything a run writes is an insert guarded by a key, so replaying it can + only produce the rows that were lost, never a duplicate. + """ + try: + self.conn.close() + except Exception: + pass + self.conn = self._connect() + cursor = self.conn.cursor() + for (statement, params) in self.pending: + cursor.execute(statement, params) + if self.pending: + print("Replayed %d statements of the interrupted transaction" % len(self.pending)) + return cursor + + def commit(self): + try: + self.conn.commit() + except self.lostConnection as e: + print("Lost the connection to the database while committing (%s); reconnecting" + % str(e).strip()) + self.recover() + self.conn.commit() + self.pending = [] + + def _migrateJobClaim(self): + """Narrow an older job_claim to (branch, libname). + + It used to be keyed by the configuration as well, so two machines whose + reference files had been updated at different times each got a claim on the + same library. Claims of a run in progress are kept, the freshest per + library, so migrating does not let a second machine in. + """ + key = [c for (c,) in self.execute("""SELECT a.attname FROM pg_index i + JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) + WHERE i.indrelid = 'job_claim'::regclass AND i.indisprimary + ORDER BY a.attnum""")] + if tuple(key) == JOB_CLAIM_KEY: + return + print("Narrowing job_claim from (%s) to (%s)" % (", ".join(key), ", ".join(JOB_CLAIM_KEY))) + cursor = self.cursor() + cursor.execute(JOB_CLAIM.replace("job_claim", "job_claim_migrated")) + cursor.execute("""INSERT INTO job_claim_migrated + SELECT DISTINCT ON (branch, libname) branch, libname, libversion, omcversion, + confighash, host, state, claimed_at, heartbeat + FROM job_claim ORDER BY branch, libname, heartbeat DESC""") + cursor.execute("DROP TABLE job_claim") + cursor.execute("ALTER TABLE job_claim_migrated RENAME TO job_claim") + self.commit() + + def sql(self, sql, hasParams=False): + """sqlite spells the placeholder "?" and psycopg2 spells it "%s". + + psycopg2 also reads "%" itself, so a literal one has to be doubled - but + only in a statement that has parameters at all. + """ + if not hasParams: + return sql + return sql.replace("%", "%%").replace("?", "%s") + + def params(self, params): + # psycopg2 only looks for placeholders when parameters are passed, so a + # statement without any must not be handed an empty tuple. + return params or None + + def quote(self, ident): + return '"%s"' % ident.replace('"', '""') + + def createTables(self, branch): + """Create the shared tables, with the keys that keep the machines apart. + + Unlike sqlite there is no schema migration and the indexes stay: other + machines are reading the table while this one writes its few thousand rows. + """ + cursor = self.cursor() + cursor.execute("""CREATE TABLE IF NOT EXISTS omcversion ( + date bigint NOT NULL, branch text NOT NULL, omcversion text)""") + cursor.execute("""CREATE TABLE IF NOT EXISTS libversion ( + date bigint NOT NULL, branch text NOT NULL, libname text NOT NULL, + libversion text, confighash bigint NOT NULL, + host text, sysinfo text)""") + # The shared database predates the host columns; add them without touching + # the rows already in there, which keep their results and read back NULL. + for col in ["host", "sysinfo"]: + cursor.execute("ALTER TABLE libversion ADD COLUMN IF NOT EXISTS %s text" % col) + cols = ", ".join("%s %s%s" % (c, POSTGRES_TYPES[t], " NOT NULL" if c in BRANCH_KEY else "") + for c, t in BRANCH_COLUMNS) + cursor.execute("CREATE TABLE IF NOT EXISTS %s (%s)" % (self.quote(branch), cols)) + for tbl in ["omcversion", "libversion", branch]: + key = KEYS.get(tbl, BRANCH_KEY) + cursor.execute("CREATE UNIQUE INDEX IF NOT EXISTS %s ON %s (%s)" + % (self.quote(("uq_%s_%s" % (tbl, "_".join(key)))[:63]), + self.quote(tbl), ",".join(key))) + cursor.execute("CREATE INDEX IF NOT EXISTS %s ON %s (libname, date)" + % (self.quote(("idx_%s_libname_date" % branch)[:63]), self.quote(branch))) + self.commit() + + def tables(self): + return [t for (t,) in self.execute( + "SELECT tablename FROM pg_tables WHERE schemaname=current_schema()")] + + def columns(self, table): + return [c for (c,) in self.execute( + "SELECT column_name FROM information_schema.columns " + "WHERE table_schema=current_schema() AND table_name=?", (table,))] + + def tableExists(self, name): + return self.execute("SELECT 1 FROM pg_tables WHERE schemaname=current_schema() AND tablename=?", + (name,)).fetchone() is not None + + def groupConcat(self, expr, orderBy=None): + if orderBy: + return "string_agg(%s::text, ',' ORDER BY %s)" % (expr, orderBy) + return "string_agg(%s::text, ',')" % expr + + def countIf(self, cond): + return "COUNT(*) FILTER (WHERE %s)" % cond + + def likeNoCase(self, column): + return "%s ILIKE ?" % column + + def insertIgnore(self): + # Another machine may have written the same run already; its row wins. + return " ON CONFLICT DO NOTHING" + + def createDateIndex(self, branch): + """Nothing to do: date is the first column of the table's unique key.""" + + def userVersion(self): + return 3 + + def setUserVersion(self, v): + pass + + def vacuum(self): + """Nothing to do: PostgreSQL has autovacuum, and a manual VACUUM of a + 50 GB database after every test run would be a waste of the machine.""" + + def claim(self, branch, libname, libversion, omcversion, confighash): + """Take the job unless another machine is running it right now. + + The claim is one row per (branch, library version, omc version, config). + A machine that dies stops sending its heartbeat, and after + STALE_CLAIM_MINUTES its jobs are up for grabs again. + """ + key = (branch, libname) + got = self.execute("""INSERT INTO job_claim + (branch, libname, libversion, omcversion, confighash, host, state) + VALUES (?,?,?,?,?,?,'running') + ON CONFLICT (branch, libname) DO UPDATE + SET host = EXCLUDED.host, state = 'running', claimed_at = now(), heartbeat = now(), + libversion = EXCLUDED.libversion, omcversion = EXCLUDED.omcversion, + confighash = EXCLUDED.confighash + WHERE job_claim.state <> 'running' + OR job_claim.heartbeat < now() - interval '%d minutes' + RETURNING host""" % STALE_CLAIM_MINUTES, + (branch, libname, libversion, omcversion, confighash, self.host)).fetchone() + self.commit() + if got is None: + return False + self.claims.append(key) + self._startHeartbeat() + return True + + def claimedBy(self, branch, libname, libversion, omcversion, confighash): + """The machine holding that job, for the message telling the user why we skip.""" + row = self.execute("SELECT host, claimed_at FROM job_claim WHERE branch=? AND libname=?", + (branch, libname)).fetchone() + return row or ("unknown", None) + + def _startHeartbeat(self): + if self.heartbeatThread is not None: + return + # A daemon thread: the run must not wait for it on the way out. + def beat(): + while self.claims: + time.sleep(HEARTBEAT_SECONDS) + try: + self._heartbeat() + except Exception as e: + print("Failed to update the job claims: %s" % e) + self.heartbeatThread = threading.Thread(target=beat, daemon=True) + self.heartbeatThread.start() + + def _heartbeat(self): + # A separate connection: the main one is in the middle of the run's work. + import psycopg2 + with psycopg2.connect(self.url) as conn: + with conn.cursor() as cur: + for (branch, libname) in list(self.claims): + cur.execute("""UPDATE job_claim SET heartbeat = now() + WHERE branch=%s AND libname=%s AND host=%s""", + (branch, libname, self.host)) + + def release(self): + for (branch, libname) in self.claims: + self.execute("""UPDATE job_claim SET state='done', heartbeat=now() + WHERE branch=? AND libname=? AND host=?""", + (branch, libname, self.host)) + self.claims = [] + self.commit() diff --git a/sanityCheck/MyLibrary/package.mo b/sanityCheck/MyLibrary/package.mo new file mode 100644 index 0000000..5d3473c --- /dev/null +++ b/sanityCheck/MyLibrary/package.mo @@ -0,0 +1,27 @@ +package MyLibrary "My awesome Modelica library" + package Mechanics + package MultiBody + package Examples + model Pendulum + extends Modelica.Mechanics.MultiBody.Examples.Elementary.Pendulum; + annotation( + experiment(StopTime=5) + ); + end Pendulum; + end Examples; + end MultiBody; + end Mechanics; + package Blocks + package Examples + model PID_Controller + extends Modelica.Blocks.Examples.PID_Controller; + annotation( + experiment(StopTime=4) + ); + end PID_Controller; + end Examples; + end Blocks; + annotation( + uses(Modelica(version = "4.0.0")), + version = "1.0.0"); +end MyLibrary; diff --git a/sanityCheck/MyOtherLibrary/package.mo b/sanityCheck/MyOtherLibrary/package.mo new file mode 100644 index 0000000..631b75b --- /dev/null +++ b/sanityCheck/MyOtherLibrary/package.mo @@ -0,0 +1,27 @@ +package MyOtherLibrary "My other awesome Modelica library" + package Mechanics + package MultiBody + package Examples + model Pendulum + extends Modelica.Mechanics.MultiBody.Examples.Elementary.Pendulum; + annotation( + experiment(StopTime=5) + ); + end Pendulum; + end Examples; + end MultiBody; + end Mechanics; + package Blocks + package Examples + model PID_Controller + extends Modelica.Blocks.Examples.PID_Controller; + annotation( + experiment(StopTime=4) + ); + end PID_Controller; + end Examples; + end Blocks; + annotation( + uses(Modelica(version = "4.0.0")), + version = "1.0.0"); +end MyOtherLibrary; diff --git a/sanityCheck/ReferenceFiles/.gitignore b/sanityCheck/ReferenceFiles/.gitignore new file mode 100644 index 0000000..5cab2f9 --- /dev/null +++ b/sanityCheck/ReferenceFiles/.gitignore @@ -0,0 +1 @@ +*.csv.hash diff --git a/sanityCheck/ReferenceFiles/MyLibrary.Blocks.Examples.PID_Controller.csv b/sanityCheck/ReferenceFiles/MyLibrary.Blocks.Examples.PID_Controller.csv new file mode 100644 index 0000000..a3b1d5d --- /dev/null +++ b/sanityCheck/ReferenceFiles/MyLibrary.Blocks.Examples.PID_Controller.csv @@ -0,0 +1,513 @@ +"time","PI.I.y","inertia1.phi","spring.phi_rel","spring.w_rel","inertia2.w","integrator.y" +0,-0.1,0,0.001,0,0,0 +0.008,-0.1,0,0.001,0,0,0 +0.016,-0.1,0,0.001,0,0,0 +0.024,-0.1,0,0.001,0,0,0 +0.032,-0.1,0,0.001,0,0,0 +0.04,-0.1,0,0.001,0,0,0 +0.048,-0.1,0,0.001,0,0,0 +0.056,-0.1,0,0.001,0,0,0 +0.064,-0.1,0,0.001,0,0,0 +0.072,-0.1,0,0.001,0,0,0 +0.08,-0.1,0,0.001,0,0,0 +0.088,-0.1,0,0.001,0,0,0 +0.096,-0.1,0,0.001,0,0,0 +0.104,-0.1,0,0.001,0,0,0 +0.112,-0.1,0,0.001,0,0,0 +0.12,-0.1,0,0.001,0,0,0 +0.128,-0.1,0,0.001,0,0,0 +0.136,-0.1,0,0.001,0,0,0 +0.144,-0.1,0,0.001,0,0,0 +0.152,-0.1,0,0.001,0,0,0 +0.16,-0.1,0,0.001,0,0,0 +0.168,-0.1,0,0.001,0,0,0 +0.176,-0.1,0,0.001,0,0,0 +0.184,-0.1,0,0.001,0,0,0 +0.192,-0.1,0,0.001,0,0,0 +0.2,-0.1,0,0.001,0,0,0 +0.208,-0.1,0,0.001,0,0,0 +0.216,-0.1,0,0.001,0,0,0 +0.224,-0.1,0,0.001,0,0,0 +0.232,-0.1,0,0.001,0,0,0 +0.24,-0.1,0,0.001,0,0,0 +0.248,-0.1,0,0.001,0,0,0 +0.256,-0.1,0,0.001,0,0,0 +0.264,-0.1,0,0.001,0,0,0 +0.272,-0.1,0,0.001,0,0,0 +0.28,-0.1,0,0.001,0,0,0 +0.288,-0.1,0,0.001,0,0,0 +0.296,-0.1,0,0.001,0,0,0 +0.304,-0.1,0,0.001,0,0,0 +0.312,-0.1,0,0.001,0,0,0 +0.32,-0.1,0,0.001,0,0,0 +0.328,-0.1,0,0.001,0,0,0 +0.336,-0.1,0,0.001,0,0,0 +0.344,-0.1,0,0.001,0,0,0 +0.352,-0.1,0,0.001,0,0,0 +0.36,-0.1,0,0.001,0,0,0 +0.368,-0.1,0,0.001,0,0,0 +0.376,-0.1,0,0.001,0,0,0 +0.384,-0.1,0,0.001,0,0,0 +0.392,-0.1,0,0.001,0,0,0 +0.4,-0.1,0,0.001,0,0,0 +0.408,-0.1,0,0.001,0,0,0 +0.416,-0.1,0,0.001,0,0,0 +0.424,-0.1,0,0.001,0,0,0 +0.432,-0.1,0,0.001,0,0,0 +0.44,-0.1,0,0.001,0,0,0 +0.448,-0.1,0,0.001,0,0,0 +0.456,-0.1,0,0.001,0,0,0 +0.464,-0.1,0,0.001,0,0,0 +0.472,-0.1,0,0.001,0,0,0 +0.48,-0.1,0,0.001,0,0,0 +0.488,-0.1,0,0.001,0,0,0 +0.496,-0.1,0,0.001,0,0,0 +0.5000000001500057,-0.1,0,0.001,0,0,0 +0.5000000001500057,-0.1,0,0.001,0,0,0 +0.504,-0.09992889782686432,8.897944473529304e-7,9.991586369599479e-4,-5.811959662615497e-4,4.653056123287788e-5,0.0039999998499942015 +0.512,-0.09945487856828177,1.7487867613111035e-5,9.856484593665843e-4,-0.0027847366346419053,9.656352387726844e-4,0.0119999998499942 +0.52,-0.09862859313104917,6.285932266043574e-5,9.570268946975345e-4,-0.004170691192961847,0.0035062327843829325,0.019999999849994413 +0.528,-0.09749786205718199,1.4178621397535734e-4,9.218291310320707e-4,-0.004483949830111507,0.007681128457368746,0.027999999849994312 +0.536,-0.09611472519729188,2.5947252677733807e-4,8.870421437913915e-4,-0.004140899014373318,0.013252654303306893,0.0359999998499942 +0.544,-0.09454325212415327,4.2232521826379347e-4,8.563051279335389e-4,-0.0035214303738221224,0.01993955209379607,0.04399999984999409 +0.552,-0.09284997753350818,6.369977579989939e-4,8.308440559429322e-4,-0.0028463213494143453,0.027487744317240735,0.05199999984999401 +0.56,-0.09109536481465991,9.095364849131691e-4,8.106254306118042e-4,-0.0022187063526533327,0.03568552504853149,0.059999999849994046 +0.568,-0.08932982850359428,0.0012449828526051269,7.950963424725627e-4,-0.0016768058542591517,0.04436150148678059,0.06799999984999394 +0.576,-0.08759312535622325,0.001647312536668378,7.835375427834965e-4,-0.0012265353468996424,0.05337854877165947,0.07599999984999395 +0.584,-0.08591511370767288,0.002119511370614021,7.752387217303736e-4,-8.60763251613486e-4,0.06262764236448354,0.08399999984999396 +0.592,-0.0843171133217085,0.0026637113308175544,7.695665805818301e-4,-5.680132703659566e-4,0.07202287916551744,0.09199999984999396 +0.6,-0.08281339352474135,0.003281339349920747,7.659840008519402e-4,-3.367994410950108e-4,0.08149714890168644,0.09999999984999397 +0.608,-0.08141261295139518,0.003973261291386205,7.640407760265192e-4,-1.5674815895244524e-4,0.09099863954164113,0.10799999984999398 +0.616,-0.08011902079123077,0.0047399020741697336,7.633639522763907e-4,-1.879167686682312e-5,0.10048809148449259,0.11599999984999398 +0.624,-0.07893344680755061,0.0055813446746015875,7.636484007711886e-4,8.476774195803785e-5,0.10993649157171995,0.12399999984999399 +0.632,-0.07785424767890593,0.006497424760536811,7.646454469369514e-4,1.604078713907405e-4,0.11932290420712066,0.131999999849994 +0.64,-0.0768778512435553,0.007487785115801524,7.661544710201325e-4,2.13468669717279e-4,0.12863307538295804,0.139999999849994 +0.648,-0.07599939993302467,0.008551939983548392,7.680137830822408e-4,2.4846138695200836e-4,0.13785793700043178,0.147999999849994 +0.656,-0.07521316755989789,0.009689316745035724,7.70093976524555e-4,2.6922423107877296e-4,0.14699252378022767,0.15599999984999402 +0.664,-0.07451284547817284,0.010899284535663223,7.722939935218341e-4,2.7896900956923614e-4,0.15603515956633052,0.16399999984999403 +0.672,-0.07389185892131218,0.012181185878777126,7.745362710748959e-4,2.802803809485493e-4,0.16498657225959454,0.17199999984999403 +0.68,-0.07334357628860777,0.013534357614306613,7.767621176246073e-4,2.7524402042650945e-4,0.17384927649968246,0.17999999984999404 +0.688,-0.07286148882297976,0.014958148866543718,7.789280173961216e-4,2.6552226985770934e-4,0.18262701633367245,0.18799999984999394 +0.696,-0.07243929908250354,0.01645192989129604,7.810022839119168e-4,2.524920545193482e-4,0.191324437700477,0.19599999984999394 +0.704,-0.07207100465244133,0.018015100447089778,7.82963030682067e-4,2.37293706406907e-4,0.1999467620322028,0.20399999984999395 +0.712,-0.071750952986344,0.019647095279280012,7.847964787345212e-4,2.2082090605436648e-4,0.208499516893283,0.21199999984999396 +0.72,-0.07147387665896235,0.021347387645341803,7.864951600903712e-4,2.0376364667449323e-4,0.21698833475160365,0.21999999984999397 +0.728,-0.07123490957255671,0.023115490935501182,7.880566297648556e-4,1.8661989039930318e-4,0.22541879754012475,0.22799999984999397 +0.736,-0.07102959050163196,0.024950959027208654,7.894819353738813e-4,1.6976309517795362e-4,0.2337963405998448,0.23599999984999398 +0.744,-0.07085386011481823,0.026853385987327238,7.907746321308616e-4,1.5348549930178145e-4,0.24212616974989068,0.243999999849994 +0.752,-0.07070404487273013,0.028822404461918385,7.919402264252733e-4,1.379981673813846e-4,0.25041320922075055,0.251999999849994 +0.76,-0.0705768408158099,0.030857684055026313,7.929854971904429e-4,1.2345145376299542e-4,0.25866205996401664,0.259999999849994 +0.768,-0.07046928572268162,0.03295892854451344,7.939183348086444e-4,1.0992182699821795e-4,0.2668769957796492,0.267999999849994 +0.776,-0.07037873899909801,0.03512587387095504,7.947471646722404e-4,9.744460633371736e-5,0.27506196438555985,0.275999999849994 +0.784,-0.07030286048185616,0.037358286018030824,7.954805550664664e-4,8.600847506154648e-5,0.2832205534116001,0.283999999849994 +0.792,-0.07023958804609234,0.03965595877325439,7.961269646377327e-4,7.559233665792417e-5,0.29135600364821346,0.29199999984999403 +0.8,-0.07018711475378647,0.04201871144282376,7.966942066386905e-4,6.615633637337239e-5,0.29947118478147067,0.29999999984999404 +0.808,-0.0701438595027661,0.04444638591652167,7.971896575890139e-4,5.765500798379456e-5,0.30756866263956767,0.30799999984999404 +0.816,-0.07010842389944076,0.04693884235498904,7.976213605306672e-4,5.002735501079635e-5,0.3156508869455182,0.31599999984999394 +0.824,-0.07007960354116843,0.04949596031796176,7.979952535665026e-4,4.3245820335815705e-5,0.32371986756651844,0.32399999984999395 +0.832,-0.07005636044989036,0.0521176360076339,7.983180339783499e-4,3.7236213944557505e-5,0.33177745684788046,0.33199999984999395 +0.84,-0.07003778735073511,0.05480377869651833,7.985955490468715e-4,3.1941998931374025e-5,0.33982528428744235,0.33999999984999396 +0.848,-0.07002309195586308,0.057554309155831075,7.988328013152338e-4,2.7323405752660697e-5,0.3478647752752844,0.34799999984999397 +0.856,-0.07001163475729337,0.060369163434774074,7.990352861912191e-4,2.3285393388036893e-5,0.35589718679440535,0.355999999849994 +0.864,-0.07000281877746624,0.06324828183559131,7.992073230778756e-4,1.9773259761117203e-5,0.3639236240249405,0.363999999849994 +0.872,-0.06999616894984746,0.06619161685162939,7.993529746291542e-4,1.6725773445260336e-5,0.37194502796114454,0.371999999849994 +0.88,-0.06999129155164642,0.06919912911060924,7.99475839108928e-4,1.4086245832137467e-5,0.37996220324440033,0.379999999849994 +0.888,-0.06998782129779695,0.07227078208402424,7.995790883539975e-4,1.1796020433983104e-5,0.3879758658256828,0.387999999849994 +0.896,-0.0699855013495829,0.0754065500880028,7.996654897788936e-4,9.821971553917825e-6,0.3959865928794046,0.395999999849994 +0.904,-0.06998407875205136,0.07860640782704961,7.997373235296287e-4,8.12924661290636e-6,0.40399490123814336,0.403999999849994 +0.912,-0.06998335764137081,0.08187033571478151,7.99796643461625e-4,6.687788068264457e-6,0.41200122029087743,0.411999999849994 +0.92,-0.06998318738231812,0.08519831868767619,7.998454156841217e-4,5.461781249990615e-6,0.4200059079630943,0.41999999984999403 +0.928,-0.06998341963997629,0.08859034191224197,7.998850700864766e-4,4.4390907407674104e-6,0.42800928586400516,0.42799999984999404 +0.936,-0.06998396169764722,0.092046396116809,7.999171053273581e-4,3.583042727389926e-6,0.43601159487090824,0.43599999984999405 +0.944,-0.06998471624212052,0.09556647157005622,7.999427786348153e-4,2.8703537846980163e-6,0.44401306429480464,0.44399999984999394 +0.952,-0.06998560616540753,0.0991505605611849,7.999631989138603e-4,2.277545888581486e-6,0.4520138839638413,0.45199999984999395 +0.96,-0.06998659041993253,0.10279865898543736,7.999791820853258e-4,1.7887857676324385e-6,0.46001418237700675,0.45999999984999396 +0.968,-0.0699876112520846,0.10651076106745251,7.999916415856148e-4,1.3791568610183194e-6,0.46801410409973887,0.46799999984999396 +0.976,-0.06998864182960479,0.11028686412400446,8.000011699492295e-4,1.0388767102922259e-6,0.4760137392028938,0.47599999984999397 +0.984,-0.06998965835300137,0.11412696577514408,8.000082778736413e-4,7.597500446006536e-7,0.4840131673614177,0.483999999849994 +0.992,-0.06999063778697655,0.11803106371734164,8.000134240727274e-4,5.336989021400616e-7,0.49201246183185793,0.4919999998499941 +1,-0.06999157416681666,0.12199915735412559,8.00016994912721e-4,3.5047354810359773e-7,0.5000116618491928,0.4999999998499941 +1.008,-0.06999245519951415,0.1260312454561953,8.000192719104245e-4,2.0910047148590148e-7,0.5080108143699826,0.5079999998499941 +1.016,-0.06999327687590727,0.13012732762263457,8.000205211225275e-4,1.0297675259702495e-7,0.5160099480719117,0.5159999998499941 +1.024,-0.06999403794246888,0.13428740372809067,8.00020991726371e-4,2.29516768229546e-8,0.5240090834289112,0.5239999998499941 +1.032,-0.06999473420220949,0.1385114733528647,8.000208996014379e-4,-3.663993699988451e-8,0.5320082430847045,0.5319999998499941 +1.04,-0.06999537091666097,0.14279953702310982,8.000203446413826e-4,-7.792764759198493e-8,0.5400074326274796,0.5399999998499941 +1.048,-0.06999594655595166,0.14715159458583885,8.00019505484953e-4,-1.0868324929831508e-7,0.5480066659688951,0.5479999998499941 +1.056,-0.06999646435206722,0.15156764636425032,8.000184810351422e-4,-1.326225519671613e-7,0.556005947367064,0.5559999998499942 +1.064,-0.06999693248622774,0.15604769317646636,8.000172862441355e-4,-1.483425220072165e-7,0.5640052708283814,0.5639999998499942 +1.072,-0.06999735443391547,0.16059173537003507,8.000159785784084e-4,-1.5774501905775434e-7,0.572004637920566,0.5719999998499942 +1.08,-0.06999773572838049,0.16519977349828152,8.000145656525448e-4,-1.592143125372829e-7,0.5800040458418151,0.5799999998499942 +1.088,-0.06999808192194969,0.1698718081164384,8.0001303018185e-4,-1.4896832976677064e-7,0.5880034914361109,0.5879999998499942 +1.096,-0.06999839139851635,0.174607839062895,8.000114748312694e-4,-1.3082110250369145e-7,0.596002984605362,0.5959999998499942 +1.104,-0.06999865333244971,0.17940786525508834,8.000101946030287e-4,-1.2386228608468808e-7,0.6040025498842457,0.6039999998499942 +1.112,-0.06999887744376156,0.18427188766501948,8.000090487972884e-4,-1.1758499852608631e-7,0.6120021729450722,0.6119999998499942 +1.12,-0.06999906429504008,0.1891999063489473,8.000080635838889e-4,-1.128533709987859e-7,0.620001855740978,0.6199999998499942 +1.128,-0.06999921404613994,0.1941919213228571,8.000072575226482e-4,-1.1031802287906429e-7,0.6280016000298223,0.627999999849994 +1.136,-0.06999933164878225,0.19924793308192126,8.000065818504186e-4,-1.0867294782912462e-7,0.6360013955660393,0.635999999849994 +1.144,-0.06999944169230005,0.20436794408507297,8.000057947760514e-4,-1.009542699869594e-7,0.6440011904331493,0.643999999849994 +1.152,-0.06999953355393973,0.20955195327003692,8.000050740364521e-4,-9.243437729652496e-8,0.652001014096662,0.651999999849994 +1.16,-0.06999961068107147,0.21479996098155008,8.000044189250597e-4,-8.318840217122961e-8,0.6600008625224947,0.659999999849994 +1.168,-0.06999967652106547,0.2201119675643494,8.000038287353128e-4,-7.329147702448166e-8,0.6680007316765637,0.667999999849994 +1.176,-0.06999973394684049,0.22548797330572684,8.000033023622601e-4,-6.29292040324873e-8,0.6760006179978149,0.675999999849994 +1.184,-0.0699997754851835,0.2309279774583611,8.000028315257472e-4,-5.427679613319899e-8,0.6840005264446463,0.683999999849994 +1.192,-0.06999980979216577,0.2364319808878593,8.000024191315281e-4,-4.640772753843976e-8,0.692000448693905,0.691999999849994 +1.2,-0.06999983930241253,0.2419999838376839,8.00002063770687e-4,-3.959188638450434e-8,0.7000003815454457,0.6999999998499941 +1.208,-0.0699998664505489,0.2476319865512975,8.000017640343094e-4,-3.409916080768746e-8,0.708000321799124,0.7079999998499941 +1.216,-0.06999989367120013,0.25332798927216255,8.000015185134795e-4,-3.019943894428384e-8,0.7160002662547945,0.7159999998499941 +1.224,-0.06999990562133672,0.2590879904659762,8.000013013602313e-4,-2.5249478189149177e-8,0.7240002324167708,0.7239999998499941 +1.232,-0.06999991096659322,0.26491199099930185,8.000011197314562e-4,-2.054020939936137e-8,0.7320002098280636,0.7319999998499941 +1.24,-0.0699999116244047,0.2707999910638829,8.00000971507255e-4,-1.625677149195942e-8,0.740000195890865,0.7399999998499941 +1.248,-0.06999990832139742,0.2767519907323822,8.000008529307154e-4,-1.2389171800154341e-8,0.7480001893942237,0.7479999998499941 +1.256,-0.06999990178419757,0.2827679900774621,8.000007602449255e-4,-8.927417657157152e-9,0.7560001891271878,0.7559999998499941 +1.264,-0.06999989273943143,0.28884798917178545,8.000006896929738e-4,-5.861516396178853e-9,0.7640001938788064,0.7639999998499941 +1.272,-0.06999988191372515,0.2949919880880147,8.00000637517948e-4,-3.1814753504304704e-9,0.7720002024381273,0.7719999998499941 +1.28,-0.06999987003370503,0.3011999868988127,8.000005999629362e-4,-8.773018531230136e-10,0.7800002135941994,0.7799999998499941 +1.288,-0.06999985782599727,0.3074719856768419,8.000005732710269e-4,1.0609967625325034e-9,0.788000226136071,0.7879999998499941 +1.296,-0.06999984601722808,0.3138079844947649,8.000005536853078e-4,2.6434131633250656e-9,0.7960002388527908,0.7959999998499941 +1.304,-0.06999983312915325,0.3202079832047574,8.000005571645103e-4,3.823471962063101e-9,0.8040002548259275,0.8039999998499942 +1.312,-0.06999981989396808,0.3266719818800388,8.000005799019244e-4,4.611308452724385e-9,0.8120002728336874,0.8119999998499942 +1.32,-0.06999980801762887,0.3331999806912049,8.000006093836059e-4,5.041996531860301e-9,0.8200002897585243,0.8199999998499942 +1.328,-0.0699997981072079,0.3397919796989628,8.000006429216774e-4,5.122467019379051e-9,0.8280003046222388,0.8279999998499942 +1.336,-0.06999979076977755,0.3464479789640197,8.000006778282609e-4,4.859650735188832e-9,0.8360003164466309,0.8359999998499942 +1.344,-0.06999978661241014,0.3531679785470829,8.00000711415479e-4,4.260478499197845e-9,0.8440003242535009,0.8439999998499942 +1.352,-0.06999978624217801,0.3599519785088596,8.000007409954535e-4,3.331881131314289e-9,0.852000327064649,0.8519999998499942 +1.36,-0.0699997902661535,0.3667999789100571,8.000007638803071e-4,2.08078945144636e-9,0.8600003239018756,0.8599999998499942 +1.368,-0.06999979929140893,0.37371197981138266,8.000007773821622e-4,5.1413427950226e-10,0.8680003137869811,0.8679999998499942 +1.376,-0.06999981392501664,0.38068798127354314,8.000007788131405e-4,-1.3611535646097571e-9,0.8760002957417652,0.875999999849994 +1.384,-0.06999982703084347,0.38772798258292585,8.00000787033124e-4,-3.241720226360044e-9,0.8840002801847723,0.883999999849994 +1.392,-0.06999982766927211,0.3948319826455686,8.000008314863143e-4,-4.678608027413585e-9,0.8920002831326582,0.891999999849994 +1.4,-0.06999982864761387,0.4019999827422028,8.0000087553406e-4,-6.131931983311939e-9,0.9000002856504487,0.899999999849994 +1.408,-0.06999982996338716,0.40923198287258,8.000009181847476e-4,-7.57142651551336e-9,0.9080002876571086,0.907999999849994 +1.416,-0.06999983161411037,0.41652798303645233,8.000009584467635e-4,-8.966826045476101e-9,0.9160002890716029,0.915999999849994 +1.424,-0.06999983359730189,0.42388798323357146,8.00000995328494e-4,-1.0287864994658424e-8,0.9240002898128963,0.923999999849994 +1.432,-0.06999983591048012,0.4313119834636892,8.000010278383259e-4,-1.1504277784518581e-8,0.9320002897999538,0.931999999849994 +1.44,-0.06999983855116337,0.4387999837265574,8.000010549846451e-4,-1.2585798836514829e-8,0.9400002889517404,0.939999999849994 +1.448,-0.06999984151687012,0.4463519840219281,8.000010757758387e-4,-1.3502162572105431e-8,0.9480002871872207,0.9479999998499941 +1.456,-0.06999984480511871,0.453967984349553,8.000010892202926e-4,-1.4223103412748634e-8,0.9560002844253598,0.9559999998499941 +1.464,-0.06999984841342755,0.4616479847091838,8.000010943263932e-4,-1.4718355779902698e-8,0.9640002805851225,0.9639999998499941 +1.472,-0.06999985233931498,0.4693919851005725,8.000010901025275e-4,-1.495765409502588e-8,0.9720002755854737,0.9719999998499941 +1.48,-0.06999985658029943,0.4771999855234709,8.000010755570814e-4,-1.4910732779576435e-8,0.9800002693453783,0.9799999998499941 +1.488,-0.06999986113389928,0.48507198597763085,8.000010496984414e-4,-1.4547326255012622e-8,0.9880002617838013,0.9879999998499941 +1.496,-0.06999986599763292,0.49300798646280414,8.000010115349939e-4,-1.3837168942792693e-8,0.9960002528197073,0.9959999998499941 +1.500000000300004,-0.06999986854209854,0.49699998701665477,8.00000992671388e-4,-1.3501704303823297e-8,1.0000002485134343,1.000000000149998 +1.500000000300004,-0.06999986854209854,0.49699998701665477,8.00000992671388e-4,-1.3501704303823297e-8,1.0000002485134343,1.000000000149998 +1.504,-0.07007096530252727,0.5010070963932937,8.008433532680682e-4,5.80766678685234e-4,1.0039536057326828,1.000000000149998 +1.512,-0.07054496837694374,0.5090544967019359,8.143568479524229e-4,0.0027828925720621246,1.0110340994066107,1.000000000149998 +1.52,-0.07137126543168586,0.5171371264086105,8.429796433720882e-4,0.004168563967522817,1.0164933990408724,1.000000000149998 +1.528,-0.07250203215678394,0.5252502030823207,8.781746360893554e-4,0.004482415671332503,1.0203186077781297,1.000000000149998 +1.536,-0.0738852184745694,0.533388521715299,9.129566110387496e-4,0.0041402392812719775,1.022747231703371,1.000000000149998 +1.544,-0.07545669720245794,0.5415456695892877,9.436932620927158e-4,0.0035215249745609964,1.0240605775888478,1.000000000149998 +1.552,-0.07714993481440666,0.5497149933516823,9.691614290397443e-4,0.0028459477903991174,1.0245123765350561,1.000000000149998 +1.56,-0.07890463618236972,0.5578904634896784,9.893702705356586e-4,0.0022189789126047734,1.0243145355505086,1.000000000149998 +1.568,-0.0806701903510261,0.5660670189077442,0.0010048906614540693,0.001678066608578789,1.0236387640803701,1.000000000149998 +1.576,-0.08240679708549205,0.574240679582391,0.0010164494232473421,0.0012280894607744092,1.0226220061716662,1.000000000149998 +1.584,-0.08408467860495532,0.5824084677355372,0.001024753978264236,8.618930635254108e-4,1.0213730938120147,1.000000000149998 +1.592,-0.08568249909279939,0.5905682497855219,0.0010304308265095129,5.688105730138799e-4,1.0199781722814125,1.000000000149998 +1.6,-0.08718597955872535,0.5987185978333145,0.001034015656739023,3.376203817709577e-4,1.018504447989584,1.000000000149998 +1.608,-0.08858666920506413,0.6068586667991481,0.001035963554435148,1.571508377091568e-4,1.0170030831039298,1.000000000149998 +1.616,-0.08988032835151243,0.6149880327149929,0.0010366431200751834,1.8760271959123695e-5,1.015513403337859,1.000000000149998 +1.624,-0.09106596386732184,0.6231065962677736,0.0010363585614909289,-8.49175898085152e-5,1.0140648501723677,1.000000000149998 +1.632,-0.09214518570771808,0.6312145184530129,0.0010353604041794437,-1.6044195021218594e-4,1.0126784313248438,1.000000000149998 +1.64,-0.09312156804614864,0.639312156688056,0.001033851492057241,-2.134477204806538e-4,1.0113683191923586,1.000000000149998 +1.648,-0.09400003243376057,0.6474000031280173,0.001031993247910932,-2.4853186982099433e-4,1.010143420622028,1.000000000149998 +1.656,-0.0947864107568846,0.6554786409615294,0.001029913379023717,-2.694926826966882e-4,1.0090084810579327,1.000000000149998 +1.664,-0.09548698654335415,0.6635486985413765,0.0010277126951518155,-2.7944839383249045e-4,1.0079652616943415,1.000000000149998 +1.672,-0.09610824950358517,0.6716108248385997,0.001025467381802773,-2.8072288408950507e-4,1.0070132573207298,1.000000000149998 +1.68,-0.09665671700152564,0.6796656715895937,0.0010232372383417053,-2.7539216833744306e-4,1.0061501987671966,1.000000000149998 +1.688,-0.09713896407808635,0.6877138962984499,0.0010210682263704703,-2.654722298521838e-4,1.0053721258508987,1.000000000149998 +1.696,-0.09756125126131378,0.6957561250179726,0.0010189915722845406,-2.5228604877048353e-4,1.0046744692029044,1.000000000149998 +1.704,-0.09792952812830942,0.7037929527058723,0.0010170306945627631,-2.3709473844571918e-4,1.0040521162023168,1.000000000149998 +1.712,-0.09824955080365934,0.7118249549746072,0.0010151974892338297,-2.2068402546657643e-4,1.0034993404275778,1.000000000149998 +1.72,-0.09852659194666329,0.7198526590901076,0.0010134991180153867,-2.0369717648451087e-4,1.0030105224352346,1.000000000149998 +1.728,-0.09876551225817243,0.7278765511224586,0.001011937755233256,-1.8657096631161156e-4,1.002580112153247,1.000000000149998 +1.736,-0.09897080768656644,0.7358970806664981,0.0010105120758694371,-1.6969592933550334e-4,1.0022025829544143,1.000000000149998 +1.744,-0.09914650560681199,0.7439146504597228,0.0010092190506034134,-1.5337019671887885e-4,1.0018728099512382,1.000000000149998 +1.752,-0.0992962842550961,0.751929628325751,0.0010080533140660749,-1.3784260523155395e-4,1.0015858395370045,1.000000000149998 +1.7533141376408368,-0.09931863907635351,0.7532460014489107,0.0010078736458509975,-1.3538663378399881e-4,1.0015424779615243,1.000000000149998 +1.7533141376408368,-0.09931863907635351,0.7532460014489107,0.0010078736458509975,-1.3538663378399881e-4,1.0015424779615243,1.000000000149998 +1.76,-0.09942348546353189,0.7599423484477946,0.0010070089405846102,-1.2333000343130813e-4,1.0013370554313774,1.000000000149998 +1.768,-0.0995312665000799,0.7679531265526494,0.001006076042567391,-1.0982103217026571e-4,1.0011218397340322,1.000000000149998 +1.776,-0.09962242805775244,0.7759622427096164,0.0010052455082910675,-9.73435743757782e-5,1.0009359941312048,1.000000000149998 +1.784,-0.09969880126838732,0.78396988003188,0.001004510599412155,-8.589957298817142e-5,1.0007766987402211,1.000000000149998 +1.792,-0.09976254276046638,0.7919762541822879,0.0010038626894525063,-7.54736968776417e-5,1.0006405696541292,1.000000000149998 +1.8,-0.09981537146087502,0.7999815370535287,0.0010032942027604398,-6.60357882054674e-5,1.0005248460111664,1.000000000149998 +1.808,-0.09985883552497585,0.8079858834611388,0.0010027977156746913,-5.754325557005605e-5,1.0004269864228952,1.000000000149998 +1.816,-0.09989417048306282,0.8159894169581475,0.001002366614977382,-4.9944457096415096e-5,1.0003448996507502,1.000000000149998 +1.824,-0.09992256412052693,0.8239922563230938,0.0010019945232141053,-4.318925180211633e-5,1.000276573429834,1.000000000149998 +1.832,-0.09994532181529828,0.831994532093771,0.0010016738415560631,-3.720752575704435e-5,1.0002197420288663,1.000000000149998 +1.84,-0.0999635308480452,0.8399963529982457,0.0010013982059262697,-3.193998709724017e-5,1.0001725150944283,1.000000000149998 +1.848,-0.09997793497336842,0.847997793411978,0.0010011620948618498,-2.731634459467951e-5,1.0001334922084768,1.000000000149998 +1.856,-0.09998929856416688,0.8559989297722578,0.0010009602084787592,-2.3273992279239477e-5,1.0001012684078276,1.000000000149998 +1.864,-0.09999809331155676,0.8639998092481967,0.0010007882661837223,-1.974868141290798e-5,1.0000748837620281,1.000000000149998 +1.872,-0.10000473504335841,0.872000473422577,0.0010006426183822256,-1.6687006931123218e-5,1.0000535033318867,1.000000000149998 +1.88,-0.10000964286488472,0.8800009642059292,0.0010005195064991454,-1.403308480256803e-5,1.0000362782459171,1.000000000149998 +1.888,-0.10001306100566866,0.8880013060212077,0.0010004164545039053,-1.1747164968441598e-5,1.0000226874746185,1.000000000149998 +1.896,-0.10001535047937118,0.8960015349697779,0.001000330176864659,-9.779904540707324e-6,1.0000119953890065,1.000000000149998 +1.904,-0.1000167022291518,0.904001670145956,0.0010002586859436363,-8.098888837876741e-6,1.0000037899003449,1.000000000149998 +1.912,-0.10001733885624535,0.9120017338098655,0.0010001997245922068,-6.6678457634533295e-6,0.9999975961222144,1.000000000149998 +1.92,-0.1000174367385739,0.9200017435992982,0.0010001513609457866,-5.454399868777328e-6,0.9999930245812105,1.000000000149998 +1.928,-0.1000171401796848,0.9280017139446093,0.0010001119236643064,-4.429775350564839e-6,0.9999897482608548,1.000000000149998 +1.936,-0.10001656726075601,0.9360016566539164,0.0010000799265915158,-3.567431122860485e-6,0.9999874886489779,1.000000000149998 +1.944,-0.10001580027844828,0.9440015799568857,0.001000054150981454,-2.844239957463111e-6,0.9999860347902753,1.000000000149998 +1.952,-0.1000149004306757,0.9520014899733084,0.0010000335666370292,-2.2398722305224933e-6,0.9999852184004668,1.000000000149998 +1.96,-0.10001395601102454,0.9600013955325433,0.0010000171790386517,-1.737428184880549e-6,0.9999848419884859,1.000000000149998 +1.968,-0.10001297294196626,0.9680012972268374,0.0010000043932248072,-1.3224192165780374e-6,0.9999848436560321,1.000000000149998 +1.976,-0.10001195256031932,0.9760011951898727,9.999947552239612e-4,-9.83491757429931e-7,0.9999851799934739,1.000000000149998 +1.984,-0.10001095337365247,0.9840010952724061,9.999874829426578e-4,-7.063431447865526e-7,0.9999857087834998,1.000000000149998 +1.992,-0.10001001085895071,0.992001001022136,9.999819945255577e-4,-4.795819810304416e-7,0.9999863338754726,1.000000000149998 +2,-0.10000908737139406,1.0000009086745802,9.999782383524362e-4,-2.9838832910438224e-7,0.9999870956693762,1.000000000149998 +2.008,-0.10000816663958943,1.0080008166025998,9.999760672462854e-4,-1.5752535936817156e-7,0.9999880001229654,1.000000000149998 +2.016,-0.10000734373339448,1.0160007343131803,9.999745876709011e-4,-4.3278815710664055e-8,0.9999888509278193,1.000000000149998 +2.024,-0.10000656701924458,1.0240006566429654,9.999739864229307e-4,4.444551496970625e-8,0.9999897272122675,1.000000000149998 +2.032,-0.10000582527938903,1.0320005824701795,9.999742060192898e-4,1.081456236218039e-7,0.999990637138554,1.000000000149998 +2.04,-0.100005127197829,1.0400005126632237,9.999751014752062e-4,1.506806709370492e-7,0.9999915569434943,1.000000000149998 +2.048,-0.10000449497865498,1.0480004494425064,9.999763590881586e-4,1.7791382847899692e-7,0.9999924311081733,1.000000000149998 +2.056,-0.10000391719304078,1.0560003916651448,9.999778799388978e-4,1.9340875083579338e-7,0.9999932642304085,1.000000000149998 +2.064,-0.10000339555771035,1.0640003395028117,9.999794959987623e-4,2.012532425228749e-7,0.999994038778329,1.000000000149998 +2.070796327151994,-0.10000297723600107,1.0707966248236542,9.999806416020466e-4,2.11349345841631e-7,0.9999946460647541,1.000000000149998 +2.070796327151994,-0.10000297723600107,1.0707966248236542,9.999806416020466e-4,2.11349345841631e-7,0.9999946460647541,1.000000000149998 +2.072,-0.10000985968662837,1.0720002614906476,0.001000009890455044,6.549568093655312e-5,0.9999932461118709,0.9987963273019923 +2.08,-0.10033866402569659,1.0799915125438049,0.0010075496211829258,0.0020428583298075314,0.9995196690712187,0.9907963273019929 +2.088,-0.10104791712875313,1.0879568084725277,0.0010317473734312499,0.003823673825099919,0.997571798829369,0.9827963273019928 +2.096,-0.10207699202319714,1.0958900865803882,0.0010656252490185935,0.004471690230947138,0.993947534360646,0.9747963273019928 +2.104,-0.10325813427882322,1.1037866974765433,0.001101054488468965,0.004199580861091417,0.9888373662953744,0.9667963273019925 +2.112,-0.10255988073993122,1.1116470832838807,0.001127725989921061,0.0023194764676419126,0.9828773448346901,0.9587963273019934 +2.12,-0.10027755771713508,1.1194752947250104,0.0011386590648696914,5.542796559825341e-4,0.9769556141401428,0.9507963273019936 +2.128,-0.09752242053840181,1.1272677722960025,0.0011391929691278839,-2.621521081552554e-4,0.9713501385288729,0.9427963273019938 +2.136,-0.09483810222557713,1.1350198427864224,0.0011363375032524261,-3.725366938304563e-4,0.965980010104047,0.9347963273019947 +2.144,-0.09233395380851865,1.142728947755306,0.0011339303181739117,-2.1412082940215814e-4,0.960699482079563,0.9267963273019953 +2.152,-0.08994358881547061,1.1503944714414762,0.0011328950570114592,-5.582532860218465e-5,0.9554189139141158,0.918796327301996 +2.16,-0.08758509813932756,1.1580166976917183,0.0011328059502687265,2.087077956036295e-5,0.9501111459362569,0.910796327301996 +2.168,-0.08521667186449267,1.1655960316729683,0.0011330552461521625,3.343255953514001e-5,0.9447819999214941,0.902796327301996 +2.176,-0.08282928165778257,1.1731327140159316,0.0011332820002849819,1.9216964273241127e-5,0.939443928119636,0.894796327301996 +2.184,-0.08042895969360446,1.1806268217130294,0.0011333707242682436,4.856855815437349e-6,0.9341058080819404,0.886796327301996 +2.192,-0.07802569794674458,1.1880783190955106,0.0011333749201226416,-1.4336079734712406e-6,0.9287703779223654,0.878796327301996 +2.2,-0.07562350856695077,1.1954871649665417,0.0011333563831724353,-2.405204279323905e-6,0.9234367207252457,0.870796327301996 +2.208,-0.07322255538452489,1.2028533429127446,0.0011333397334478432,-1.5397428521142045e-6,0.9181036758799603,0.862796327301996 +2.216,-0.07082244312491852,1.2101768490063813,0.0011333308625335959,-4.132678279198601e-7,0.9127707180382133,0.854796327301996 +2.224,-0.06842272223616372,1.2174576839096436,0.0011333287759292046,3.591902313360516e-7,0.9074376421909172,0.8467963273019959 +2.232,-0.06602301572364903,1.2246958491312834,0.0011333312055486708,4.793922329451351e-7,0.9021043489247849,0.8387963273019959 +2.24,-0.06362313815330706,1.2318913473828341,0.001133334081076673,2.48188281444443e-7,0.8967709385234003,0.8307963273019959 +2.248,-0.06122310457383563,1.2390441798762362,0.0011333355969092322,-3.7701777362675004e-8,0.8914375098933507,0.8227963273019959 +2.256,-0.05882298401500467,1.246154346879748,0.0011333353553912135,-2.0357998378007493e-7,0.8861041212672778,0.8147963273019964 +2.264,-0.05642286135976601,1.2532218479004618,0.0011333340943251407,-2.1896927641737046e-7,0.880770782804186,0.8067963273019962 +2.272,-0.054022771377174726,1.2602466822090204,0.0011333328992837045,-1.897292525364417e-7,0.8754374592175409,0.7987963273019962 +2.28,-0.0516227543152494,1.2672288494707658,0.00113333227046385,-1.2415267386837055e-7,0.8701041477430808,0.7907963273019961 +2.288,-0.049222764270791156,1.2741683500164032,0.001133331719956719,-2.4877960595810807e-8,0.8647708475013312,0.7827963273019961 +2.296,-0.04682283496899639,1.281065183454605,0.001133331831800384,7.626199710921665e-8,0.8594375478813223,0.774796327301996 +2.304,-0.04442289392281882,1.2879193501279176,0.0011333320910365643,1.3573048608672156e-7,0.8541042343708248,0.766796327301996 +2.312,-0.04202294515440111,1.29473084989683,0.0011333327071276346,1.2871422290573806e-7,0.8487708986987409,0.758796327301996 +2.32,-0.03962298048407871,1.301499683016451,0.0011333332970531688,9.909108797780793e-8,0.8434375554910314,0.7507963273019959 +2.328,-0.03722298627361886,1.3082258495700751,0.0011333337356406485,4.2886029810007526e-8,0.8381042034226798,0.7427963273019959 +2.336,-0.034822974156614915,1.3149093495800632,0.0011333339896315623,-1.5480510185387935e-8,0.8327708506338334,0.7347963273019957 +2.344,-0.03242295262700539,1.3215501830953051,0.00113333398588507,-5.0839695746194525e-8,0.8274375055141049,0.7267963273019957 +2.352,-0.030022923552996933,1.3281483500712166,0.0011333337911796093,-7.011037725861732e-8,0.8221041657572106,0.7187963273019955 +2.36,-0.02762289779905494,1.3347038503684048,0.0011333336145822963,-8.656555451204533e-8,0.8167708269388175,0.7107963273019955 +2.368,-0.02522287903206255,1.3412166840027837,0.0011333334322531333,-9.050509874745403e-8,0.8114374922923019,0.7027963273019955 +2.376,-0.02282287331411888,1.3476868509385862,0.0011333332978591,-7.987389349927289e-8,0.8061041625027026,0.6947963273019955 +2.384,-0.020422881142739338,1.3541143511636138,0.0011333332295779294,-5.657403787981058e-8,0.8007708369359867,0.6867963273019952 +2.392,-0.018022878506107818,1.3604991847694938,0.0011333330892741888,-3.993727446998863e-8,0.7954375091482402,0.6787963273019952 +2.4,-0.015622880769600755,1.3668413516810483,0.0011333329899483704,-2.1292524780378692e-8,0.7901041820298226,0.6707963273019952 +2.408,-0.013222888533398014,1.3731408518856454,0.001133332950432971,-2.468878838274421e-9,0.7847708549710372,0.6627963273019952 +2.416,-0.010822902397679465,1.379397685370654,0.0011333329895604866,1.4704573329030487e-8,0.7794375273621876,0.6547963273019952 +2.424,-0.00842291243287074,1.385611852200847,0.0011333330117402423,2.8081796916274943e-8,0.7741041984879283,0.646796327301995 +2.432,-0.0060229207246772,1.3917833523526728,0.0011333330519474214,3.5878416094170376e-8,0.7687708677534677,0.638796327301995 +2.44,-0.003622927681370298,1.3979121858149106,0.0011333331269291196,3.625956440878898e-8,0.7634375345471834,0.630796327301995 +2.448,-0.0012229324134633843,1.4039983525858797,0.0011333332393301456,2.7351312987134663e-8,0.7581041982444329,0.622796327301995 +2.456,0.0011770633643814943,1.410041852686101,0.0011333333579069621,1.667041310432669e-8,0.7527708613507994,0.6147963273019951 +2.464,0.0035770580261296216,1.4160426861353401,0.0011333334525693575,1.1414021992144372e-8,0.7474375262653356,0.6067963273019951 +2.472,0.005977053553933097,1.4220008529226007,0.0011333335401812316,5.768008758168037e-9,0.7421041910499978,0.5987963273019951 +2.48,0.008377050149416181,1.4279163530520658,0.0011333336144360925,3.3890797510218886e-10,0.7367708559069641,0.590796327301995 +2.488,0.010777048014203152,1.4337891865279198,0.001133333669027449,-4.266745784348657e-9,0.7314375210384128,0.582796327301995 +2.496,0.013177047349918269,1.4396193533543458,0.0011333336976488086,-7.442417947480006e-9,0.7261041866465218,0.574796327301995 +2.504,0.015577048358185804,1.4454068535355273,0.0011333336939936792,-8.581573941587351e-9,0.7207708529334697,0.566796327301995 +2.512,0.017977051240630027,1.451151687075648,0.00113333365175557,-7.077679193966176e-9,0.7154375201014344,0.558796327301995 +2.52,0.0203770561988752,1.4568538539788916,0.0011333335646279885,-2.3241991319119853e-9,0.710104188352594,0.550796327301995 +2.528,0.022777058614046507,1.462513354171831,0.0011333335428420858,-3.1133090469887313e-9,0.704770854756224,0.5427963273019951 +2.536,0.02517706044661316,1.4681301876869672,0.0011333335375705367,-5.409187748304613e-9,0.6994375206575977,0.5347963273019951 +2.544,0.02757706252541389,1.4737043545385826,0.0011333335273436683,-7.382396794147615e-9,0.694104186666528,0.5267963273019951 +2.552,0.02997706478337179,1.479235854726534,0.0011333335123509564,-8.95029849152551e-9,0.6887708528105607,0.5187963273019951 +2.56,0.03237706715340994,1.4847246882506793,0.0011333334927818757,-1.0030255147446062e-8,0.6834375191172418,0.5107963273019951 +2.568,0.03477706956845143,1.4901708551108759,0.0011333334688259007,-1.0539629068917045e-8,0.6781041856141171,0.5027963273019951 +2.576,0.037177071961419374,1.495574355306981,0.0011333334406725068,-1.0395782562946229e-8,0.6727708523287327,0.494796327301995 +2.584,0.03957707426523683,1.500935188838852,0.0011333334085111692,-9.516077936541381e-9,0.6674375192886343,0.486796327301995 +2.592,0.04197707640774762,1.5062533557052873,0.0011333333743088288,-8.065909778849943e-9,0.6621041864386903,0.47879632730199506 +2.6,0.044377078283522976,1.511528855898144,0.001133333351676056,-7.83545477452417e-9,0.6567708531821754,0.47079632730199505 +2.608,0.0467770798534828,1.5167616894231195,0.0011333333310049474,-7.374923636273376e-9,0.6514375200023524,0.46279632730199505 +2.616,0.04917708104897797,1.5219518562797436,0.0011333333130351592,-6.678452452013748e-9,0.6461041869011762,0.45479632730199504 +2.624,0.051577081801359406,1.5270993564675455,0.0011333332985063486,-5.74017730966148e-9,0.6407708538806013,0.44679632730199503 +2.632,0.05397708204197801,1.5322041899860543,0.0011333332881581718,-4.554234297132762e-9,0.6354375209425822,0.438796327301995 +2.64,0.05637708170218466,1.5372663568348008,0.001133333282730286,-3.114759502343785e-9,0.630104188089074,0.430796327301995 +2.648,0.058777080713330265,1.5422858570133136,0.0011333332829623476,-1.41588901321074e-9,0.6247708553220307,0.422796327301995 +2.656,0.061177079006765736,1.5472626905211218,0.0011333332895940135,5.48241082350183e-10,0.6194375226434075,0.414796327301995 +2.664,0.0635770775343214,1.5521968573635454,0.001133333294203816,2.414738787631019e-9,0.61410418993224,0.4067963273019951 +2.672,0.06597707711693804,1.5570883575451597,0.00113333328954532,3.868009816262073e-9,0.6087708570833305,0.3987963273019951 +2.68,0.06837707665440845,1.5619371910596422,0.0011333332856187624,5.286677300234554e-9,0.6034375242228864,0.3907963273019951 +2.688,0.07077707616484522,1.5667433579070151,0.0011333332823849156,6.645253332858926e-9,0.5981041913424118,0.3827963273019951 +2.696,0.07317707566636096,1.5715068580872997,0.001133333279804554,7.91825000744566e-9,0.5927708584334107,0.3747963273019951 +2.704,0.07557707517706821,1.5762276916005178,0.0011333332778384496,9.08017941730522e-9,0.5874375254873871,0.3667963273019951 +2.712,0.07797707471507963,1.5809058584466913,0.0011333332764473759,1.0105553655748074e-8,0.5821041924958453,0.35879632730199507 +2.72,0.08037707429850778,1.5855413586258416,0.0011333332755921065,1.0968884816084693e-8,0.576770859450289,0.35079632730199506 +2.728,0.08277707394546524,1.5901341921379903,0.001133333275233414,1.1644684991625536e-8,0.5714375263422226,0.34279632730199505 +2.736,0.08517707367406464,1.5946843589831596,0.0011333332753320719,1.2107466275681077e-8,0.5661041931631496,0.33479632730199504 +2.744,0.08757707350241858,1.599191859161371,0.0011333332758488531,1.2331740761561781e-8,0.5607708599045745,0.32679632730199504 +2.752,0.08997707344863948,1.6036566926726459,0.0011333332767445312,1.2292020542578123e-8,0.5554375265580014,0.3187963273019955 +2.76,0.09237707353084024,1.6080788595170066,0.0011333332779798787,1.1962817712040572e-8,0.5501041931149339,0.31079632730199547 +2.768,0.09477707376713329,1.6124583596944744,0.0011333332795156693,1.1318644363259585e-8,0.5447708595668762,0.30279632730199546 +2.776,0.09717707417563122,1.616795193205071,0.001133333281312676,1.0334012589545634e-8,0.5394375259053323,0.29479632730199545 +2.784,0.09957707477444667,1.6210893600488185,0.0011333332833316722,8.983434484209181e-9,0.5341041921218065,0.28679632730199545 +2.792,0.10197707558169221,1.6253408602257386,0.0011333332855334305,7.241422140560698e-9,0.5287708582078025,0.27879632730199544 +2.8,0.10437707654957372,1.6295496937358123,0.0011333332879021384,5.192857973461714e-9,0.5234375241916145,0.27079632730199554 +2.808,0.10677707640266382,1.633715860578268,0.001133333290858112,4.978476042301165e-9,0.5181041907868205,0.26279632730199554 +2.816,0.10917707625399552,1.6378393607537993,0.0011333332939731434,4.7391841080703416e-9,0.5127708573737233,0.25479632730199553 +2.824,0.11157707610465567,1.6419201942624175,0.001133333297214054,4.478006316665427e-9,0.5074375239533307,0.24679632730199555 +2.832,0.11397707595573113,1.6459583611041333,0.0011333333005476647,4.1979668139826064e-9,0.5021041905266509,0.23879632730199557 +2.84,0.11637707580830883,1.6499538612789588,0.0011333333039407992,3.902089745918062e-9,0.49677085709469193,0.23079632730199554 +2.848,0.11877707566347558,1.6539066947869039,0.0011333333073602772,3.5933992583679793e-9,0.49143752365846183,0.22279632730199553 +2.856,0.12117707552231831,1.6578168616279814,0.001133333310772922,3.2749194972285437e-9,0.4861041902189686,0.21479632730199552 +2.864,0.12357707538592388,1.6616843618022017,0.0011333333141455542,2.9496746083959383e-9,0.48077085677722037,0.20679632730199551 +2.872,0.12597707525537916,1.6655091953095762,0.001133333317444996,2.6206887377663457e-9,0.4754375233342251,0.1987963273019955 +2.88,0.12837707513177102,1.6692913621501166,0.0011333333206380689,2.2909860312359527e-9,0.4701041898909909,0.1907963273019955 +2.888,0.1307770750161864,1.673030862323834,0.001133333323691595,1.963590634700941e-9,0.4647708564485258,0.1827963273019955 +2.896,0.1331770749097121,1.676727695830739,0.0011333333265723953,1.6415266940574977e-9,0.45943752300783786,0.17479632730199549 +2.904,0.13557707481343503,1.6803818626708436,0.0011333333292472922,1.3278183552018047e-9,0.4541041895699351,0.16679632730199548 +2.912,0.13797707472844206,1.6839933628441592,0.0011333333316831075,1.0254897640300464e-9,0.4487708561358255,0.15879632730199547 +2.92,0.14037707465582006,1.6875621963506966,0.0011333333338466626,7.37565066438408e-10,0.4434375227065173,0.15079632730199546 +2.928,0.14277707459665592,1.691088363190467,0.001133333335704779,4.670684083230725e-10,0.4381041892830184,0.14279632730199546 +2.936,0.14517707455203654,1.694571863363482,0.0011333333372242787,2.1702393558022485e-10,0.43277085586633696,0.13479632730199545 +2.944,0.1475770744727944,1.6980126968703364,0.0011333333398023102,-1.0892314255617305e-10,0.4274375224243545,0.12679632730199528 +2.952,0.14997707437459787,1.7014108637108718,0.001133333342990677,-4.789132386180131e-10,0.42210418896769114,0.11879632730199527 +2.96,0.15237707428076117,1.7047663638848425,0.0011333333461235775,-8.459676347476587e-10,0.4167708555120064,0.11079632730199526 +2.968,0.15477707419166523,1.7080791973922662,0.0011333333491879204,-1.2084578562880208e-9,0.41143752205784306,0.10279632730199526 +2.976,0.15717707410769133,1.7113493642331636,0.0011333333521706156,-1.5647554285820106e-9,0.4061041886057438,0.09479632730199525 +2.984,0.15957707402922044,1.714576864407554,0.0011333333550585718,-1.9132318769725393e-9,0.40077085515625177,0.08679632730199521 +2.992,0.16197707395663385,1.7177616979154566,0.001133333357838698,-2.252258726802518e-9,0.3954375217099095,0.07879632730199523 +3,0.16437707389031242,1.7209038647568913,0.001133333360497904,-2.580207503414857e-9,0.3901041882672599,0.07079632730199523 +3.008,0.16677707383063747,1.7240033649318767,0.0011333333630230977,-2.895449732152468e-9,0.38477085482884593,0.06279632730199522 +3.016,0.16917707377798996,1.7270601984404337,0.0011333333654011896,-3.1963569383582623e-9,0.37943752139521014,0.05479632730199521 +3.024,0.1715770737327511,1.7300743652825805,0.0011333333676190882,-3.4813006473751505e-9,0.3741041879668956,0.046796327301995205 +3.032,0.17397707369530196,1.7330458654583367,0.0011333333696637025,-3.748652384546044e-9,0.3687708545444451,0.0387963273019952 +3.04,0.17637707366602365,1.7359746989677225,0.001133333371521942,-3.996783675213853e-9,0.3634375211284013,0.030796327301995195 +3.048,0.17877707364529724,1.7388608658107572,0.0011333333731807155,-4.224066044721489e-9,0.35810418771930724,0.022796327301995187 +3.056,0.1811770736335039,1.7417043659874596,0.0011333333746269322,-4.428871018411864e-9,0.35277085431770555,0.014796327301995182 +3.064,0.1835770736310247,1.7445051994978498,0.0011333333758475014,-4.609570121627888e-9,0.3474375209241393,0.006796327301995202 +3.0707963272520318,0.18561597181212994,1.7468511019101483,0.0011333333766973804,-4.742927884280969e-9,0.3429066360449989,4.9963454704035784e-11 +3.0707963272520318,0.18561597181212994,1.7468511019101483,0.0011333333766973804,-4.742927884280969e-9,0.3429066360449989,4.9963454704035784e-11 +3.072,0.18591488953570995,1.747263366337416,0.0011333333710527632,-4.628092476230108e-9,0.34210418758463174,4.9963454704035784e-11 +3.08,0.18551011751238447,1.7499788665366511,0.0011333333410040215,-2.6841029628517492e-9,0.3367708548992955,4.9963454704035784e-11 +3.088,0.18268449687506652,1.7526517000583897,0.001133333327991599,-7.12652196918451e-10,0.33143752222311196,4.9963454704035784e-11 +3.096,0.1787715204445563,1.755281866905518,0.0011333333268348377,2.476786581306446e-10,0.32610418920988893,4.9963454704035784e-11 +3.104,0.1743697476531017,1.7578693670832295,0.001133333329831367,4.087666818805983e-10,0.3207708559302513,4.9963454704035784e-11 +3.112,0.16974800883411817,1.7604142005944303,0.0011333333325420601,2.474292326810822e-10,0.31543752254313845,4.9963454704035784e-11 +3.12,0.1650291832753384,1.7629163674400035,0.0011333333337495812,7.089601086863196e-11,0.3101041891509604,4.9963454704035784e-11 +3.128,0.16026553123160814,1.7653758676196154,0.0011333333338692504,-1.4404706407849672e-11,0.3047708557891933,4.9963454704035784e-11 +3.136,0.1554813621107516,1.7677927011327834,0.001133333333639157,-3.028616391672475e-11,0.29943752245056604,4.9963454704035784e-11 +3.144,0.150688010259363,1.7701668679791989,0.001133333333453621,-2.3878373398720714e-11,0.2941041891193687,4.9963454704035784e-11 +3.152,0.14589119641346648,1.7724983681588782,0.0011333333333648826,-1.4104475933972698e-11,0.28877085578929335,4.9963454704035784e-11 +3.16,0.1410926143820765,1.7747872016719042,0.0011333333333200047,-4.8576690867976515e-12,0.28343752245904236,4.9963454704035784e-11 +3.168,0.13629325583633917,1.7770333685182733,0.0011333333333022626,7.336538034517667e-13,0.27810418912757284,4.9963454704035784e-11 +3.176,0.13149360224588116,1.7792368686979725,0.0011333333333035684,2.6787529347682737e-12,0.27277085579488786,4.9963454704035784e-11 +3.184,0.12669377698353693,1.7813977022109895,0.001133333333316453,2.257286030841847e-12,0.2674375224614141,4.9963454704035784e-11 +3.192,0.1218938229205503,1.7835158690573338,0.001133333333331169,8.689403355695771e-13,0.2621041891276182,4.9963454704035784e-11 +3.2,0.11709379338068919,1.785591369237022,0.0011333333333384372,-2.558562337008656e-13,0.2567708557939099,4.9963454704035784e-11 +3.208,0.11229374241146928,1.7876242027500457,0.0011333333333408047,-1.0467110548994943e-12,0.25143752246031303,4.9963454704035784e-11 +3.216,0.10749372145756711,1.7896143695964006,0.001133333333338924,-1.2423946104042127e-12,0.24610418912691454,4.9963454704035784e-11 +3.224,0.10269370292007045,1.7915618697760898,0.001133333333334581,-1.1304989182365895e-12,0.24077085579361845,4.9963454704035784e-11 +3.232,0.09789367018998871,1.7934667032891132,0.0011333333333304221,-1.0280100023044886e-12,0.23543752246031927,4.9963454704035784e-11 +3.24,0.09309364156493177,1.79532887013547,0.001133333333327121,-8.566436503397237e-13,0.23010418912704306,4.9963454704035784e-11 +3.248,0.08829362097206378,1.7971483703151612,0.0011333333333261627,-7.008415927328148e-13,0.22477085579376166,4.9963454704035784e-11 +3.256,0.08349361168419021,1.7989252038281844,0.0011333333333254714,-4.727704361066669e-13,0.21943752246050457,4.9963454704035784e-11 +3.264,0.07869361722451759,1.8006593706745395,0.0011333333333243354,-1.505208422616583e-13,0.21410418912727866,4.9963454704035784e-11 +3.272,0.07389364159023687,1.8023508708542264,0.0011333333333246208,1.6302935299592205e-13,0.20877085579404983,4.9963454704035784e-11 +3.28,0.06909368851285673,1.803999704367244,0.0011333333333267482,4.3494924859877094e-13,0.20343752246080712,4.9963454704035784e-11 +3.288,0.06429372058732463,1.8056058712135963,0.0011333333333278402,7.129870124346519e-13,0.19810418912756636,4.9963454704035784e-11 +3.296,0.05949374870895341,1.807169371393282,0.0011333333333288925,9.501617349551417e-13,0.1927708557943121,4.9963454704035784e-11 +3.304,0.054693772400585865,1.808690204906301,0.0011333333333299882,1.1217967597067176e-12,0.18743752246103593,4.9963454704035784e-11 +3.312,0.04989378876585216,1.8101683717526538,0.001133333333331017,1.207960110513466e-12,0.18210418912773133,4.9963454704035784e-11 +3.32,0.045093794908382466,1.8116038719323402,0.0011333333333318678,1.1887198111994727e-12,0.17677085579439156,4.9963454704035784e-11 +3.328,0.040293787931806956,1.8129967054453602,0.0011333333333324303,1.0441438855888243e-12,0.17143752246101007,4.9963454704035784e-11 +3.336,0.03549376493975579,1.8143468722917142,0.0011333333333325936,7.543003575056068e-13,0.16610418912758007,4.9963454704035784e-11 +3.344,0.030693748559939504,1.8156543724714012,0.0011333333333329036,5.07298555610112e-13,0.1607708557941643,4.9963454704035784e-11 +3.352,0.025893750766392977,1.8169192059844204,0.0011333333333336333,4.044119807948258e-13,0.15543752246079667,4.9963454704035784e-11 +3.36,0.021093752933662478,1.8181413728307725,0.0011333333333342656,2.975042689887243e-13,0.1501041891274277,4.9963454704035784e-11 +3.368,0.016293755373691127,1.8193208730104586,0.0011333333333347739,1.9279371473971278e-13,0.14477085579405946,4.9963454704035784e-11 +3.376,0.011493758398422059,1.8204577065234775,0.0011333333333351295,9.649861259569643e-14,0.13943752246069402,4.9963454704035784e-11 +3.384,0.006693762319798402,1.82155187336983,0.0011333333333353045,1.483725710458084e-14,0.13410418912733346,4.9963454704035784e-11 +3.392,0.0018937674497632824,1.8226033735495153,0.0011333333333352709,-4.5972057185728687e-14,0.12877085579397984,4.9963454704035784e-11 +3.4,-0.0029062265625400745,1.823612207062534,0.001133333333335053,-8.579764649278282e-14,0.12343752246063311,4.9963454704035784e-11 +3.408,-0.007706225192201997,1.8245783739088863,0.0011333333333350833,-1.5156459703733073e-13,0.11810418912727785,4.9963454704035784e-11 +3.416,-0.012506224305583916,1.8255018740885718,0.0011333333333350295,-2.0195929027197152e-13,0.11277085579392772,4.9963454704035784e-11 +3.424,-0.017306223899267986,1.8263827076015906,0.0011333333333348886,-2.335966753208599e-13,0.10743752246058384,4.9963454704035784e-11 +3.432,-0.02210622396983636,1.827220874447943,0.0011333333333346566,-2.430917013081507e-13,0.10210418912724734,4.9963454704035784e-11 +3.44,-0.02690622451387119,1.8280163746276286,0.0011333333333343302,-2.2705931735799857e-13,0.09677085579391934,4.9963454704035784e-11 +3.448,-0.03170622552795464,1.8287692081406473,0.0011333333333339054,-1.8211447259455828e-13,0.09143752246060098,4.9963454704035784e-11 +3.456,-0.03650622700866885,1.8294793749869995,0.0011333333333333792,-1.0487211614198463e-13,0.0861041891272934,4.9963454704035784e-11 +3.464,-0.041306227816030126,1.830146875166685,0.0011333333333331292,-6.347841959405506e-14,0.08077085579397379,4.9963454704035784e-11 +3.472,-0.04610622829405139,1.8307717086797037,0.0011333333333330474,-4.2042267754849886e-14,0.07543752246064761,4.9963454704035784e-11 +3.48,-0.05090622884816612,1.8313538755260557,0.0011333333333329995,-1.6279265500746645e-14,0.07010418912732284,4.9963454704035784e-11 +3.488,-0.055706229475294494,1.831893375705741,0.0011333333333329863,1.3671033936901246e-14,0.0647708557939996,4.9963454704035784e-11 +3.496,-0.06050283859992084,1.832390211172791,0.0011333314402239172,-3.1198198875940242e-6,0.05943762285983822,4.9963454704035784e-11 +3.504,-0.06505048156800618,1.8328450446211986,0.0011327591226836947,-1.6720177156714659e-4,0.054140579292182675,4.9963454704035784e-11 +3.512,-0.06919804160951086,1.8332598003824516,0.0011304063270393765,-4.238045941630538e-4,0.04898882177230963,4.9963454704035784e-11 +3.52,-0.07296199893046078,1.8336361962324745,0.0011260688973635513,-6.515068188459555e-4,0.04407001292220107,4.9963454704035784e-11 +3.528,-0.07635955053686691,1.8339759513982243,0.001120140582445959,-8.202059059490823e-4,0.03943783980369733,4.9963454704035784e-11 +3.536,-0.07941016131039273,1.8342810124493565,0.0011131000357753995,-9.307950999963892e-4,0.03512214088760046,4.9963454704035784e-11 +3.544,-0.08213549770277995,1.8345535461155582,0.0011053756562467987,-9.92581183487181e-4,0.031137314245096533,4.9963454704035784e-11 +3.552,-0.08455875661223708,1.834795872024474,0.0010973160173148486,-0.0010167082757491068,0.027486036038727915,4.9963454704035784e-11 +3.56,-0.08670384348162548,1.8350103807109726,0.0010891842422355409,-0.0010123745829424245,0.02416292455859585,4.9963454704035784e-11 +3.568,-0.08859463043416595,1.8351994594036203,0.0010811761410347493,-9.869080973754674e-4,0.021156942239976276,4.9963454704035784e-11 +3.576,-0.09025447990620837,1.8353654443526208,0.0010734347886402174,-9.463344607933186e-4,0.01845288066975798,4.9963454704035784e-11 +3.584,-0.09170583879607179,1.8355105802450296,0.0010660613348383776,-8.955059066975729e-4,0.016032873587583894,4.9963454704035784e-11 +3.592,-0.09297003570841944,1.8356369999451723,0.0010591213125242405,-8.381076129140889e-4,0.01387734214505934,4.9963454704035784e-11 +3.6,-0.09406692878504479,1.835746689254707,0.001052657388001267,-7.77150935026354e-4,0.011966292328270171,4.9963454704035784e-11 +3.608,-0.09501508934245273,1.8358415053103014,0.0010466879997592295,-7.147918243197788e-4,0.01027920681553207,4.9963454704035784e-11 +3.616,-0.09583157512445306,1.8359231538893377,0.0010412171320470867,-6.527060825972788e-4,0.008796009382797638,4.9963454704035784e-11 +3.624,-0.09653196475296595,1.8359931928505309,0.0010362378925413156,-5.9221885374244e-4,0.007497328852748213,4.9963454704035784e-11 +3.632,-0.09713046424746007,1.8360530427999995,0.0010317331343916846,-5.342229281000781e-4,0.00636455198184553,4.9963454704035784e-11 +3.64,-0.09763987162095876,1.836103983538122,0.0010276802879711968,-4.7934687965266583e-4,0.005380247166733456,4.9963454704035784e-11 +3.648,-0.09807167092818486,1.8361471634696245,0.0010240528239612008,-4.279818634294187e-4,0.004528209722432978,4.9963454704035784e-11 +3.656,-0.09843612859850534,1.8361836092375956,0.0010208215559540366,-3.8033450696196207e-4,0.0037934500537231803,4.9963454704035784e-11 +3.664,-0.09874236961662076,1.8362142333399252,0.0010179562995674884,-3.364950961631811e-4,0.003162238671894263,4.9963454704035784e-11 +3.672,-0.09899847360547853,1.8362398437390282,0.001015426542683545,-2.964504164549065e-4,0.0026220684710462415,4.9963454704035784e-11 +3.68,-0.09921156427433894,1.8362611528061137,0.001013202354675815,-2.601192107944236e-4,0.002161627009513779,4.9963454704035784e-11 +3.688,-0.09938789922228404,1.836278786301157,0.0010112545826749888,-2.2734931040595938e-4,0.0017707252226447602,4.9963454704035784e-11 +3.696,-0.09953294909766065,1.8362932912890813,0.001009555435043887,-1.979447321581225e-4,0.001440247081653535,4.9963454704035784e-11 +3.704,-0.09965146990622413,1.8363051433705695,0.0010080785829392554,-1.7168462726007246e-4,0.0011620630873882727,4.9963454704035784e-11 +3.712,-0.09974758661847366,1.8363147550426946,0.0010067995758374618,-1.4833686470735511e-4,9.289712100297773e-4,4.9963454704035784e-11 +3.72,-0.09982486481502274,1.836322482863468,0.00100569594845636,-1.2766654693103976e-4,7.34623501528761e-4,4.9963454704035784e-11 +3.728,-0.09988638233090041,1.836328634616144,0.001004747267252424,-1.0943004422138023e-4,5.734641249445147e-4,4.9963454704035784e-11 +3.736,-0.09993481566137609,1.8363334779499512,0.0010039357536675067,-9.342217976968461e-5,4.406495021760108e-4,4.9963454704035784e-11 +3.744,-0.09997238375653493,1.8363372347600158,0.001003243944760348,-7.941916590210802e-5,3.318975691769823e-4,4.9963454704035784e-11 +3.752,-0.10000101084148846,1.836340097468979,0.0010026565514032438,-6.722774328758992e-5,2.4347707615027809e-4,4.9963454704035784e-11 +3.76,-0.10002238649210861,1.8363422350345109,0.0010021605642706421,-5.668947280283683e-5,1.7214988330743544e-4,4.9963454704035784e-11 +3.768,-0.10003780406187968,1.8363437767917137,0.0010017432311758634,-4.7579567332187115e-5,1.1521673406029088e-4,4.9963454704035784e-11 +3.776,-0.1000485037800373,1.8363448467637407,0.0010013939976035895,-3.975632208588879e-5,7.02099931453648e-5,4.9963454704035784e-11 +3.784,-0.10005546422646999,1.8363455428086464,0.0010011032247752097,-3.306537690793836e-5,3.507135818297091e-5,4.9963454704035784e-11 +3.792,-0.10005941808334078,1.8363459381945886,0.001000862260445529,-2.7354145054603775e-5,8.125362189963928e-6,4.9963454704035784e-11 +3.8,-0.10006111981943214,1.8363461083685328,0.0010006635612398915,-2.249306757980242e-5,-1.2216579983615084e-5,4.9963454704035784e-11 +3.808,-0.10006105658238318,1.8363461020452385,0.0010005007698447098,-1.837179951145089e-5,-2.7130158285695717e-5,4.9963454704035784e-11 +3.816,-0.10005967850121382,1.8363459642375766,0.0010003682865420273,-1.4891295002766703e-5,-3.767297033622056e-5,4.9963454704035784e-11 +3.824,-0.10005738672806212,1.836345735060707,0.001000261247311127,-1.1965391378308468e-5,-4.477528499729553e-5,4.9963454704035784e-11 +3.832,-0.10005440305447175,1.836345436693842,0.0010001755623852886,-9.513008031869247e-6,-4.907923181002021e-5,4.9963454704035784e-11 +3.84,-0.1000510014922229,1.8363450965380868,0.001000107742744257,-7.476742258080244e-6,-5.124158371832532e-5,4.9963454704035784e-11 +3.848,-0.10004736161807133,1.8363447325511004,0.0010000547926142037,-5.79914722827367e-6,-5.1754942617757916e-5,4.9963454704035784e-11 +3.856,-0.10004359405957107,1.836344355795664,0.0010000140879775979,-4.419624310047125e-6,-5.0987919105185716e-5,4.9963454704035784e-11 +3.864,-0.10003984802800682,1.836343981192904,9.9998339876753e-4,-3.2977343842799363e-6,-4.932546025034873e-5,4.9963454704035784e-11 +3.872,-0.1000361728441486,1.8363436136748872,9.999609520831036e-4,-2.392451707787325e-6,-4.699015774996253e-5,4.9963454704035784e-11 +3.88,-0.10003263770747571,1.8363432601615803,9.99945092941717e-4,-1.6655858874965014e-6,-4.421423435388225e-5,4.9963454704035784e-11 +3.888,-0.10002930453969401,1.83634292684518,9.999343604704537e-4,-1.082645962470442e-6,-4.120278329296442e-5,4.9963454704035784e-11 +3.896,-0.10002616662129288,1.8363426130537113,9.999278892321162e-4,-6.257543827996338e-7,-3.80328971544205e-5,4.9963454704035784e-11 +3.904,-0.10002326146638241,1.8363423225386055,9.99924584042153e-4,-2.6873578647967213e-7,-3.48492669846203e-5,4.9963454704035784e-11 +3.912,-0.10002059374600053,1.8363420557669674,9.999237009236622e-4,6.1249041693040044e-9,-3.172695224971547e-5,4.9963454704035784e-11 +3.92,-0.10001815113960402,1.8363418115067376,9.99924724385707e-4,2.1175502408081286e-7,-2.869925236697651e-5,4.9963454704035784e-11 +3.928,-0.10001593901968635,1.8363415902951543,9.999270871062567e-4,3.590452228053021e-7,-2.582849628392165e-5,4.9963454704035784e-11 +3.936,-0.10001393926351539,1.8363413903199524,9.99930438580736e-4,4.608895049615037e-7,-2.3122437873217553e-5,4.9963454704035784e-11 +3.944,-0.10001214286737611,1.8363412106807528,9.999344493311878e-4,5.260214445789666e-7,-2.060050615643768e-5,4.9963454704035784e-11 +3.952,-0.10001054025388952,1.83634105041981,9.99938849377278e-4,5.622771905986385e-7,-1.827531224332366e-5,4.9963454704035784e-11 +3.96,-0.10000911038770387,1.836340907433594,9.999434898736626e-4,5.758723172027091e-7,-1.6135675141266173e-5,4.9963454704035784e-11 +3.968,-0.10000784543264239,1.836340780938487,9.99948169822375e-4,5.74465064052624e-7,-1.4188239000679728e-5,4.9963454704035784e-11 +3.976,-0.10000673085316279,1.8363406694809348,9.99952782516357e-4,5.622621387340513e-7,-1.2425362191426133e-5,4.9963454704035784e-11 +3.984,-0.10000575009509949,1.836340571405521,9.999572750006632e-4,5.40861030457115e-7,-1.0833800184566996e-5,4.9963454704035784e-11 +3.992,-0.10000489509989528,1.8363404859063959,9.999615386494058e-4,5.142387571164946e-7,-9.413977369999223e-6,4.9963454704035784e-11 +4,-0.10000414949313693,1.8363404113461148,9.999655743871774e-4,4.836900237039136e-7,-8.145849202793757e-6,4.9963454704035784e-11 +4,-0.10000414949313693,1.8363404113461148,9.999655743871774e-4,4.836900237039136e-7,-8.145849202793757e-6,4.9963454704035784e-11 diff --git a/sanityCheck/ReferenceFiles/MyLibrary.Mechanics.MultiBody.Examples.Pendulum.csv b/sanityCheck/ReferenceFiles/MyLibrary.Mechanics.MultiBody.Examples.Pendulum.csv new file mode 100644 index 0000000..3d0f8d6 --- /dev/null +++ b/sanityCheck/ReferenceFiles/MyLibrary.Mechanics.MultiBody.Examples.Pendulum.csv @@ -0,0 +1,503 @@ +"time","rev.phi","rev.w" +0,0,0 +0.01,-9.752110835132235e-4,-0.19496302970410784 +0.02,-0.003895863368354152,-0.3891502982706474 +0.03,-0.008754636788601214,-0.5825617364350218 +0.04,-0.015543992927360403,-0.7751931594213085 +0.05,-0.024255824474940263,-0.9670349096445527 +0.06,-0.03488202754978809,-1.1580668993521737 +0.07,-0.04741435119708346,-1.3482591608506387 +0.08,-0.06184424853665106,-1.5375702457696012 +0.09,-0.07816263949095557,-1.7259425180728638 +0.1,-0.09635974541500349,-1.9133012075669449 +0.11,-0.11642491626502394,-2.0995534240069014 +0.12,-0.13834653629675533,-2.284586879765275 +0.13,-0.16211185958453167,-2.468268556150011 +0.14,-0.18770652752064904,-2.650440690358422 +0.15,-0.21511456496343587,-2.830921882927834 +0.16,-0.24431810994226336,-3.009505332866342 +0.17,-0.27529703403888445,-3.1859572411727948 +0.18,-0.30802874753468873,-3.36001691964371 +0.19,-0.34248786558855454,-3.5313962202522053 +0.2,-0.3786461704471993,-3.699779863642325 +0.21,-0.41647205154255484,-3.864825447236649 +0.22,-0.45593020649380933,-4.0261646154208215 +0.23,-0.4969816771963918,-4.183404259287226 +0.24,-0.5395833546423394,-4.336128166253466 +0.25,-0.5836878289230405,-4.48389961318294 +0.26,-0.6292434004000951,-4.626264273122351 +0.27,-0.6761936145857617,-4.762753419523473 +0.28,-0.7244773459831648,-4.8928878010645 +0.29,-0.7740286745852644,-5.016182194009252 +0.3,-0.824776699204791,-5.132150030882257 +0.31,-0.8766457940459409,-5.240309061968026 +0.32,-0.9295554740285055,-5.340186403764211 +0.33,-0.9834205749627553,-5.431324631005059 +0.34,-1.0381515660270912,-5.513288086828955 +0.35,-1.0936545910745048,-5.585668077376969 +0.36,-1.1498319365906982,-5.648089199646527 +0.37,-1.2065823548864985,-5.700215162446754 +0.38,-1.2638013915967736,-5.74175279456605 +0.39,-1.3213817362020654,-5.77245863964002 +0.4,-1.379214191505122,-5.792140089038206 +0.41,-1.4371876020516912,-5.800662882741665 +0.42,-1.4951900824097673,-5.797947224897506 +0.43,-1.5531090392237201,-5.783977099496616 +0.44,-1.6108321701787252,-5.758791514772839 +0.45,-1.6682477782866716,-5.722492906969946 +0.46,-1.7252454831065316,-5.675239286221293 +0.47,-1.7817167415872526,-5.617244421236039 +0.48,-1.8375554337596882,-5.54877616341482 +0.49,-1.8926583173741696,-5.4701478142697395 +0.5,-1.9469255901418074,-5.381719249823689 +0.51,-2.0002611779437505,-5.283886367762868 +0.52,-2.0525731727429126,-5.17707924793544 +0.53,-2.103774144669076,-5.061754813312258 +0.54,-2.1537812144120574,-4.938390845584172 +0.55,-2.2025165484141533,-4.807481253453063 +0.56,-2.2499071207567405,-4.669529109872016 +0.57,-2.2958850884614064,-4.525042074216982 +0.58,-2.340387645493919,-4.374526628213282 +0.59,-2.3833569533627297,-4.218484449992786 +0.6,-2.424740309288396,-4.057406244630795 +0.61,-2.464489679158017,-3.891770616233972 +0.62,-2.5025618681100994,-3.722038270309158 +0.63,-2.5389181103388725,-3.54865138575561 +0.64,-2.5735239741773404,-3.372030797462589 +0.65,-2.6063491550738402,-3.192573377417217 +0.66,-2.637367142219717,-3.0106533771676007 +0.67,-2.6665550983747637,-2.8266186480625315 +0.68,-2.6938935000573188,-2.6407933983167338 +0.69,-2.719365963422733,-2.453476339470051 +0.7,-2.7429589579684026,-2.2649418345133165 +0.71,-2.7646615896643207,-2.0754414573654243 +0.72,-2.784465349936405,-1.8852032506407987 +0.73,-2.802363924356915,-1.6944351523134438 +0.74,-2.8183529613010916,-1.5033247441229602 +0.75,-2.8324298955313325,-1.3120418662551234 +0.76,-2.8445938043701537,-1.1207402206222608 +0.77,-2.8548451743835344,-0.9295586191729044 +0.78,-2.8631858769614285,-0.7386238257883189 +0.79,-2.869618944944872,-0.5480517113692844 +0.8,-2.874148557233519,-0.3579497970626227 +0.81,-2.8767799349678795,-0.1684189837126262 +0.82,-2.8775192486722276,0.020444361718332894 +0.83,-2.8763736753870863,0.20854636903618415 +0.84,-2.8733512627977964,0.3957937318553607 +0.85,-2.86846104928363,0.5820917457899034 +0.86,-2.861713009448713,0.767342427522495 +0.87,-2.8531181155329257,0.9514426109974398 +0.88,-2.8426884432831536,1.1342823557219577 +0.89,-2.830437149649668,1.3157429406142491 +0.9,-2.8163786859522024,1.4956956547204394 +0.91,-2.8005288080272575,1.6739999719919092 +0.92,-2.7829047554827775,1.850502343686891 +0.93,-2.7635253918535923,2.0250351834635 +0.94,-2.742411289690838,2.197415162430825 +0.95,-2.7195849798581255,2.367443250870674 +0.96,-2.695071033049546,2.5349031257921433 +0.97,-2.668896290698048,2.699561405512431 +0.98,-2.6410900082528515,2.8611672761302454 +0.99,-2.6116840345053336,3.019452035887016 +1,-2.580712994552113,3.1741306313005695 +1.01,-2.5482144466211167,3.324900772807621 +1.02,-2.5142290475896494,3.471445481831102 +1.03,-2.4788006942899212,3.6134334039145397 +1.04,-2.4419766744299003,3.750520663589478 +1.05,-2.403807732091489,3.8823538343164805 +1.06,-2.3643482434218326,4.008570454842697 +1.07,-2.32365617824548,4.128804022069033 +1.08,-2.281793229092675,4.242685130523921 +1.09,-2.2388247710207083,4.349845897138327 +1.1,-2.1948197832725933,4.449923598820651 +1.11,-2.1498509437421065,4.542563219445445 +1.12,-2.1039942966072385,4.627423115161043 +1.13,-2.0573293304947047,4.704177414640815 +1.14,-2.0099386276985007,4.772521100645248 +1.15,-1.9619077030670538,4.832173181500655 +1.16,-1.9133248108727212,4.882880887204689 +1.17,-1.8642804706932592,4.924422599391435 +1.18,-1.8148673668777653,4.956611767882183 +1.19,-1.7651797676266685,4.9792987571581175 +1.2,-1.7153132924914318,4.992373693318895 +1.21,-1.6653644506733818,4.995768443627075 +1.22,-1.6154301842612095,4.989456259925659 +1.23,-1.5656075427363607,4.973455210337068 +1.24,-1.5159931318755873,4.947824683801906 +1.25,-1.4666827792704806,4.9126677245525014 +1.26,-1.4177710546255542,4.868128114673349 +1.27,-1.3693509195420481,4.81438812583518 +1.28,-1.321513278380007,4.751668927977043 +1.29,-1.2743467088506446,4.680223920978285 +1.3,-1.2279370381319545,4.600339196009413 +1.31,-1.1823671282787223,4.512327446385922 +1.32,-1.137716601258222,4.416525583936646 +1.33,-1.0940615414481505,4.3132909127286245 +1.34,-1.0514744808789798,4.202996360620249 +1.35,-1.01002403707778,4.086027672300872 +1.36,-0.9697750258960031,3.9627786848126214 +1.37,-0.9307882460209154,3.833648301821337 +1.38,-0.8931204880909451,3.699036522754963 +1.39,-0.8568246216215107,3.559341982670109 +1.4,-0.8219494395314421,3.41495788853949 +1.41,-0.788539944226855,3.2662708320896074 +1.42,-0.7566372452451972,3.1136572759705725 +1.43,-0.7262787735406276,2.9574823341398324 +1.44,-0.6974983948770405,2.7980986205626794 +1.45,-0.6703264618514995,2.6358433654303304 +1.46,-0.6447900888089805,2.47103990467227 +1.47,-0.6209131924682622,2.303994862468624 +1.48,-0.5987167287405409,2.134999553914581 +1.49,-0.5782188094997216,1.9643292693147225 +1.5,-0.5594348749865875,1.7922431905515144 +1.51,-0.5423778473726841,1.6189860708195145 +1.52,-0.5270582807787613,1.4447873662246242 +1.53,-0.5134844959033606,1.2698635901082052 +1.54,-0.5016627152706227,1.0944183197794926 +1.55,-0.49159719171879374,0.9186437319146684 +1.56,-0.48329028397471246,0.742722076192065 +1.57,-0.4767426113103671,0.5668263589096535 +1.58,-0.4719530627603924,0.3911225311141091 +1.59,-0.4689189272023414,0.21577030431287508 +1.6,-0.4676359115764876,0.040924996338840725 +1.61,-0.46809816413754585,-0.13326111709511546 +1.62,-0.47029836719144824,-0.30663738020726417 +1.63,-0.47422764536841594,-0.479053193558242 +1.64,-0.47987565563184387,-0.6503566661842572 +1.65,-0.48723049646394434,-0.8203932255491184 +1.66,-0.4962787046739076,-0.9890043849981381 +1.67,-0.5070052282120291,-1.1560263483228497 +1.68,-0.5193933110626373,-1.3212892149258169 +1.69,-0.5334245016852214,-1.4846155242643426 +1.7,-0.5490785016742565,-1.6458198284472332 +1.71,-0.566333131035863,-1.8047076402251119 +1.72,-0.5851642163205182,-1.9610748473417503 +1.73,-0.6055454814747194,-2.1147077155865346 +1.74,-0.6274484810214388,-2.2653818121703 +1.75,-0.6508424576558415,-2.412863005123825 +1.76,-0.6756942725786319,-2.5569067418256948 +1.77,-0.7019682857171723,-2.6972589285314506 +1.78,-0.7296262798303285,-2.833656808480137 +1.79,-0.758627352869058,-2.9658287508676553 +1.8,-0.7889278707684871,-3.093496912714576 +1.81,-0.8204813703486951,-3.2163770830342715 +1.82,-0.8532385376280147,-3.3341813958294995 +1.83,-0.8871471732278985,-3.446619669516327 +1.84,-0.9221521415558781,-3.553401106790841 +1.85,-0.9581954589735963,-3.6542374791280885 +1.86,-0.9952162165007691,-3.7488442191840115 +1.87,-1.0331507457296043,-3.836944173630186 +1.88,-1.0719326244969771,-3.9182693400575284 +1.89,-1.1114927979537963,-3.992563928509309 +1.9,-1.1517597803835389,-4.059587061344718 +1.91,-1.192659675342998,-4.119115157822788 +1.92,-1.234116534688287,-4.170944776766414 +1.93,-1.2760524243617102,-4.214894864127981 +1.94,-1.31838774474199,-4.25080916795085 +1.95,-1.3610414945778568,-4.278557738810307 +1.96,-1.403931444502738,-4.298039706472136 +1.97,-1.4469745781482442,-4.309183240432242 +1.98,-1.4900872578389286,-4.311948122874866 +1.99,-1.5331856371107089,-4.306325254035551 +2,-1.5761859246553,-4.292337357402013 +2.01,-1.6190047138485775,-4.2700396052155405 +2.02,-1.6615593119478078,-4.239517414919217 +2.03,-1.703768033472698,-4.200887906450319 +2.04,-1.745550508209574,-4.154296397536788 +2.05,-1.7868279593428213,-4.099916464040507 +2.06,-1.827523490988332,-4.0379475521316905 +2.07,-1.8675622837935268,-3.9686122763973866 +2.08,-1.9068719147216238,-3.892155644262649 +2.09,-1.9453824444895642,-3.808840759776133 +2.1,-1.9830266969658865,-3.7189477484585867 +2.11,-2.0197403535723595,-3.6227702057512943 +2.12,-2.0554620511432966,-3.520612899418941 +2.13,-2.0901336034389675,-3.4127892414017587 +2.14,-2.1236998963786697,-3.2996185489196437 +2.15,-2.1561091071207628,-3.1814237115499098 +2.16,-2.187312600164155,-3.0585289341775437 +2.17,-2.217264987707604,-2.931257754655108 +2.18,-2.2459241372669676,-2.7999307085156095 +2.19,-2.2732510453114894,-2.6648644696119455 +2.2,-2.2992099087392064,-2.5263692769221047 +2.21,-2.323767942374007,-2.384748835782656 +2.22,-2.346895385480107,-2.2402985199440777 +2.23,-2.368565383421714,-2.093304797803672 +2.24,-2.3887538845557454,-1.9440452226922038 +2.25,-2.4074395888004974,-1.7927868885540665 +2.26,-2.424603796240718,-1.6397877528623914 +2.27,-2.440230350551359,-1.485295442974899 +2.28,-2.454305515818654,-1.3295481950260815 +2.29,-2.466817898253562,-1.172775212120217 +2.3,-2.4777583431730124,-1.015196457317606 +2.31,-2.487119866191742,-0.8570243519968654 +2.32,-2.4948975571825054,-0.6984635007382578 +2.33,-2.501088526902923,-0.5397122226926774 +2.34,-2.505691841988998,-0.38096306390003937 +2.35,-2.5087084554395864,-0.22240372561327634 +2.36,-2.5101412020066243,-0.06421827187267865 +2.37,-2.5099947130501983,0.09341222746109294 +2.38,-2.50827544561934,0.25030840774898716 +2.39,-2.504991632085814,0.40629185260740974 +2.4,-2.500153286307209,0.5611840164927439 +2.41,-2.4937722309708743,0.714805437901269 +2.42,-2.485862061201826,0.8669747325437156 +2.43,-2.476438228370325,1.0175080072319354 +2.44,-2.46551801762684,1.166217967192059 +2.45,-2.4531206151056617,1.312913396989485 +2.46,-2.4392671475268393,1.4573988069657977 +2.47,-2.423980702423606,1.5994735769061748 +2.48,-2.407286417462142,1.7389323658490752 +2.49,-2.389211495871366,1.8755643061764233 +2.5,-2.3697852843493257,2.0091535532719575 +2.51,-2.349039308372498,2.1394792662017914 +2.52,-2.327007329568727,2.2663156849565382 +2.53,-2.303725382167134,2.3894333055086427 +2.54,-2.2792318231356092,2.508598615631342 +2.55,-2.2535673491916905,2.623575859981014 +2.56,-2.226775028154512,2.7341273775187327 +2.57,-2.198900315172568,2.840014948660854 +2.58,-2.169991021175738,2.9410014435101157 +2.59,-2.140097356702187,3.036851436004334 +2.6,-2.109271829365871,3.1273337619800956 +2.61,-2.077569262784436,3.212222330865263 +2.62,-2.0450466978823023,3.291298379688491 +2.63,-2.0117632952333797,3.364352158826084 +2.64,-1.9777803157261122,3.4311844847251436 +2.65,-1.9431608879364344,3.4916089829610746 +2.66,-1.907969987625707,3.5454535243876437 +2.67,-1.8722741978134145,3.592562179746957 +2.68,-1.8361415787618784,3.6327966422643674 +2.69,-1.7996415031066304,3.6660380251911886 +2.7,-1.7628443874642228,3.6921877214886214 +2.71,-1.7258215753916974,3.7111692443458617 +2.72,-1.6886450224939864,3.7229284563913185 +2.73,-1.6513871314324635,3.727434812460001 +2.74,-1.6141204863345646,3.7246817988398564 +2.75,-1.5769176065889674,3.71468654555163 +2.76,-1.5398507344598105,3.6974910974790376 +2.77,-1.5029915603455963,3.6731605494985278 +2.78,-1.466411012575398,3.6417839019934584 +2.79,-1.4301790098258156,3.6034724798817614 +2.8,-1.394364263373316,3.558358880394179 +2.81,-1.359034028742871,3.506596790674935 +2.82,-1.3242539612773117,3.4483581120140885 +2.83,-1.2900878766682657,3.3838328744018806 +2.84,-1.2565976308543205,3.3132265327601598 +2.85,-1.2238429542477567,3.236758796668136 +2.86,-1.1918812896982125,3.1546617982142924 +2.87,-1.160767752236954,3.0671779717890493 +2.88,-1.1305549309608909,2.9745586536560205 +2.89,-1.1012929142390349,2.8770619317408217 +2.9,-1.073029155947994,2.7749511703654504 +2.91,-1.0458084551608715,2.6684931592200734 +2.92,-1.019672964886112,2.557956847853662 +2.93,-0.9946620913911431,2.443611462830335 +2.94,-0.9708126035609449,2.3257257624659586 +2.95,-0.9481585546992811,2.204566362912534 +2.96,-0.9267313616950128,2.0803970187692213 +2.97,-0.9065598323589746,1.9534779108515241 +2.98,-0.8876701655556698,1.824064261590305 +2.99,-0.8700860640644101,1.692406771745978 +3,-0.8538287267398923,1.558750232193743 +3.01,-0.8389169480821256,1.423334012521155 +3.02,-0.8253671522933086,1.2863915787644629 +3.03,-0.8131934578081221,1.148150350147082 +3.04,-0.8024077399272734,1.008832350705308 +3.05,-0.7930196805888682,0.868653667310268 +3.06,-0.7850368269450436,0.727825495861249 +3.07,-0.7784646388001697,0.5865539989479055 +3.08,-0.7733065399692288,0.4450409707602052 +3.09,-0.7695639468840446,0.3034844356244406 +3.1,-0.7672363278020514,0.16207886084282408 +3.11,-0.7663212051009592,0.02101613509250846 +3.12,-0.766814205578772,-0.1195141742812963 +3.13,-0.7687090645140416,-0.2593240267344465 +3.14,-0.7719976347265693,-0.3982264306425154 +3.15,-0.776669919796626,-0.5360348434545275 +3.16,-0.7827140376916176,-0.6725626813533944 +3.17,-0.7901162553203027,-0.8076227741433929 +3.18,-0.798860952086343,-0.9410270039006372 +3.19,-0.8089306226729159,-1.0725859452047228 +3.2,-0.820305864350687,-1.2021084368117902 +3.21,-0.8329653447385177,-1.329401669641944 +3.22,-0.8468858033848342,-1.454270655905193 +3.23,-0.8620420124940344,-1.5765186412066907 +3.24,-0.8784067717160049,-1.6959468809689235 +3.25,-0.8959508814698294,-1.812354898879126 +3.26,-0.9146431343189356,-1.9255410483586182 +3.27,-0.9344502943828451,-2.0353023566358925 +3.28,-0.955337101452822,-2.1414357646074293 +3.29,-0.9772662581424343,-2.243738148574367 +3.3,-1.0001984409240314,-2.342007437215524 +3.31,-1.0240923251967315,-2.436043487540785 +3.32,-1.048904573654224,-2.5256485854886814 +3.33,-1.0745899187872419,-2.61062915901306 +3.34,-1.1011011541505007,-2.690796226584862 +3.35,-1.128389230516113,-2.7659670650011914 +3.36,-1.1564033058454664,-2.835966155745449 +3.37,-1.185090806192331,-2.9006264482808444 +3.38,-1.2143975677100867,-2.9597907557066034 +3.39,-1.2442678692059999,-3.0133127983509436 +3.4,-1.2746446207986146,-3.0610585886436215 +3.41,-1.305469439860745,-3.102907467377903 +3.42,-1.3366828080476258,-3.1387533234914184 +3.43,-1.3682242439112882,-3.1685053636088614 +3.44,-1.4000324039755194,-3.1920894019869617 +3.45,-1.4320453222543406,-3.2094481303134903 +3.46,-1.4642005239343965,-3.2205422126550016 +3.47,-1.49643524316788,-3.2253505023014077 +3.48,-1.5286865981874622,-3.2238701956322253 +3.49,-1.5608917605535682,-3.216117673462588 +3.5,-1.592988171275532,-3.202127461508548 +3.51,-1.6249137018696287,-3.181953075760044 +3.52,-1.6566068547210009,-3.155665853186616 +3.53,-1.6880069292899311,-3.1233548183451467 +3.54,-1.7190542099893962,-3.085126184401983 +3.55,-1.7496901095055253,-3.041101914951946 +3.56,-1.7798573575195804,-2.9914196944304794 +3.57,-1.8095001136353386,-2.9362310065568793 +3.58,-1.838564128940709,-2.8757006564204164 +3.59,-1.8669968675888966,-2.8100053023781126 +3.6,-1.894747590898059,-2.739332149159673 +3.61,-1.9217675192373527,-2.663878012961684 +3.62,-1.9480098430546984,-2.5838476425021772 +3.63,-1.97342986525621,-2.499452771907091 +3.64,-1.99798502334392,-2.4109106787062884 +3.65,-2.0216349363221364,-2.3184431492369253 +3.66,-2.044341494813556,-2.2222751530088165 +3.67,-2.066068809540391,-2.122634017794835 +3.68,-2.086783306652586,-2.019748163362269 +3.69,-2.1064536756475922,-1.9138464431951274 +3.7,-2.1250508987232446,-1.8051572525609947 +3.71,-2.1425482469763057,-1.6939076650312825 +3.72,-2.1589212334796013,-1.580323231432827 +3.73,-2.174147638914901,-1.464626906478044 +3.74,-2.188207441414315,-1.3470392302927288 +3.75,-2.2010828203730064,-1.227777599275377 +3.76,-2.2127581105887497,-1.1070562006533742 +3.77,-2.223219768489289,-0.9850860740355015 +3.78,-2.232456350574323,-0.8620745876794275 +3.79,-2.240458464810842,-0.7382260683323529 +3.8,-2.247218749145468,-0.613741388230529 +3.81,-2.2527318326158183,-0.4888184325735233 +3.82,-2.256994311647077,-0.3636522501758146 +3.83,-2.260004714401563,-0.23843509626800066 +3.84,-2.2617634862360987,-0.11335703459303677 +3.85,-2.2622729558981014,0.01139407810164504 +3.86,-2.2615373262755645,0.13563206066255487 +3.87,-2.259562654081892,0.2591722334889851 +3.88,-2.256356832384577,0.38183106418183804 +3.89,-2.2519295930419223,0.5034259051340768 +3.9,-2.246292482548999,0.623774718361829 +3.91,-2.2394588777253674,0.7426959158477039 +3.92,-2.2314439578228393,0.8600080268743377 +3.93,-2.2222647270960794,0.9755299315488385 +3.94,-2.211939983875566,1.0890801385757858 +3.95,-2.2004903620225438,1.2004774762277188 +3.96,-2.187938287204959,1.3095401957797517 +3.97,-2.1743080088412254,1.4160871013480014 +3.98,-2.159625589171238,1.5199369847025854 +3.99,-2.143918896399524,1.6209091729487577 +4,-2.1272176070754245,1.718824705617424 +4.01,-2.1095531969768566,1.8135049068660734 +4.02,-2.090958873180092,1.9047740809289484 +4.03,-2.071469647284133,1.9924578782610245 +4.04,-2.051122203080646,2.0763857210362753 +4.05,-2.0299549696833425,2.1563899086724994 +4.06,-2.0080079714379266,2.23230784410281 +4.07,-1.985322899134192,2.303981333372478 +4.08,-1.9619429244749946,2.371258963206213 +4.09,-1.9379127775301939,2.433995505656378 +4.1,-1.91327852034142,2.4920540344746644 +4.11,-1.8880876281298797,2.5453058494726566 +4.12,-1.8623887281800413,2.593631931279965 +4.13,-1.8362316763876412,2.6369234842066613 +4.14,-1.8096672726468557,2.675082735294575 +4.15,-1.7827473232384872,2.70802391711242 +4.16,-1.75552434541974,2.735673500449035 +4.17,-1.7280516071900318,2.7579714918529485 +4.18,-1.7003828328828543,2.774871051882277 +4.19,-1.6725722145978572,2.786340105592273 +4.2,-1.6446741311162303,2.7923602461097614 +4.21,-1.6167431253669327,2.79292865189552 +4.22,-1.5888336511699666,2.788056279917036 +4.23,-1.561000010875364,2.777769982179709 +4.24,-1.5332961442352817,2.7621101268573116 +4.25,-1.5057755226769771,2.741132736946185 +4.26,-1.4784909908488109,2.714906737403687 +4.27,-1.4514946182944737,2.6835159436418086 +4.28,-1.4248375991411344,2.647056132771259 +4.29,-1.3985700658691023,2.605636744436798 +4.3,-1.372741048320842,2.559377968048886 +4.31,-1.3473982567401577,2.5084120443136686 +4.32,-1.3225880978190943,2.452880559651208 +4.33,-1.2983554358179614,2.3929352589222734 +4.34,-1.2747436599558906,2.3287357214580684 +4.35,-1.2517944338242786,2.2604496320551832 +4.36,-1.2295478051286337,2.1882509688566683 +4.37,-1.208041954477842,2.1123197302757277 +4.38,-1.1873133361211892,2.03284070536593 +4.39,-1.1673964370151497,1.9500027039750747 +4.4,-1.1483239360316884,1.8639979221334473 +4.41,-1.1301264840653218,1.77502076453842 +4.42,-1.1128328654941495,1.6832677505725044 +4.43,-1.0964698143841778,1.5889360772309833 +4.44,-1.0810621608968256,1.492223942647111 +4.45,-1.066632690098182,1.3933290152425226 +4.46,-1.053202267021605,1.2924490391474948 +4.47,-1.0407897367317633,1.1897803536370704 +4.48,-1.0294120255609938,1.085518653780097 +4.49,-1.0190840782819248,0.9798576706830425 +4.5,-1.0098189355713358,0.872989980011387 +4.51,-1.0016277022000863,0.7651059153158298 +4.52,-0.9945196027305934,0.6563943414396173 +4.53,-0.9885019736078935,0.5470418357561115 +4.54,-0.9835803003869318,0.43723336813669417 +4.55,-0.9797582264407118,0.32715175229223004 +4.56,-0.9770375757014648,0.21697819598443774 +4.57,-0.9754183712220201,0.10689199868848477 +4.58,-0.9748988476290781,-0.002929046084986538 +4.59,-0.9754754737917374,-0.11230874952585125 +4.6,-0.9771429590460898,-0.22107227710120989 +4.61,-0.979894275574696,-0.32904609861168005 +4.62,-0.9837206620662193,-0.43605788341586255 +4.63,-0.9886116429475711,-0.5419363628443842 +4.64,-0.9945550325777375,-0.6465113578983284 +4.65,-1.0015369500680906,-0.7496136159424075 +4.66,-1.0095418264567333,-0.8510749533883697 +4.67,-1.018552415385457,-0.9507281251010752 +4.68,-1.0285498050243425,-1.0484070621840311 +4.69,-1.039513426250569,-1.1439468253382428 +4.7,-1.0514210704376714,-1.2371839169449044 +4.71,-1.064248897923943,-1.3279563591482686 +4.72,-1.0779714621801857,-1.4161040597880536 +4.73,-1.0925617221953448,-1.5014690425502717 +4.74,-1.1079910730215623,-1.5838958479076055 +4.75,-1.1242293661805827,-1.6632319266808497 +4.76,-1.1412449461857856,-1.7393280600070449 +4.77,-1.1590046831349086,-1.8120389096981568 +4.78,-1.1774740145525513,-1.8812234453088832 +4.79,-1.196616993952806,-1.9467456257542002 +4.8,-1.2163963372523694,-2.0084748265487984 +4.81,-1.236773490258655,-2.066286608671581 +4.82,-1.2577086789816385,-2.120063144499233 +4.83,-1.2791609977634104,-2.1696940137085847 +4.84,-1.301088463058165,-2.2150766315637727 +4.85,-1.3234481223459027,-2.2561169997905863 +4.86,-1.3461961113246947,-2.292730144749749 +4.87,-1.3692877819681053,-2.3248407457082365 +4.88,-1.3926777635285545,-2.35238359207497 +4.89,-1.4163201062691593,-2.3753040139026558 +4.9,-1.440168347137114,-2.393558366020481 +4.91,-1.46417566401627,-2.4071141966705434 +4.92,-1.4882949473083171,-2.4159507617298206 +4.93,-1.5124789582068172,-2.4200588867308253 +4.94,-1.5366804076354563,-2.4194415067212915 +4.95,-1.5608521112090221,-2.414113202891251 +4.96,-1.584947076952491,-2.4041007539445465 +4.97,-1.6089186494116416,-2.3894423583539686 +4.98,-1.6327206072754534,-2.37018817295726 +4.99,-1.6563072895793785,-2.346399261897039 +5,-1.6796337037798856,-2.318148089367275 +5,-1.6796337037798856,-2.318148089367275 diff --git a/shared.py b/shared.py index 5056352..a5afa80 100644 --- a/shared.py +++ b/shared.py @@ -1,50 +1,72 @@ #!/usr/bin/env python3 -import re, os, subprocess +import re, os, string, subprocess import simplejson as json -def fixData(data,rmlStyle,abortSimulationFlag,alarmFlag,overrideDefaults,defaultCustomCommands,extrasimflags): +simCodeTargetRe = re.compile('--simCodeTarget=([^"\'\\s,;)]+)') + +def simCodeTargetFromCommands(target, commands): + for cmd in commands: + found = simCodeTargetRe.findall(str(cmd)) + if found: + target = found[-1] + return target + +def fixData(data,abortSimulationFlag,alarmFlag,overrideDefaults,defaultCustomCommands,extrasimflags,environmentTranslation,environmentSimulation): data["configFromFile"] = dict(data) for (key,default) in overrideDefaults: if key not in data: data[key] = default try: + data["runOnceBeforeTesting"] = (data.get("runOnceBeforeTesting") or []) data["simCodeTarget"] = data.get("simCodeTarget") or "C" data["referenceFileExtension"] = data.get("referenceFileExtension") or "mat" data["referenceFileNameDelimiter"] = data.get("referenceFileNameDelimiter") or "." - data["default_tolerance"] = float(data.get("default_tolerance") or 1e-6) + data["defaultTolerance"] = float(data.get("defaultTolerance") or 1e-6) + data["defaultNumberOfIntervals"] = int(data.get("defaultNumberOfIntervals") or 2500) data["reference_reltol"] = float(data.get("reference_reltol") or 3e-3) data["reference_reltolDiffMinMax"] = float(data.get("reference_reltolDiffMinMax") or 3e-3) data["reference_rangeDelta"] = float(data.get("reference_rangeDelta") or 1e-3) - debug = "+d" if rmlStyle else "-d" if data["simCodeTarget"]=="Cpp": defaultCustomCommands2 = defaultCustomCommands[:] - defaultCustomCommands2.append('setCommandLineOptions("%ssimCodeTarget=Cpp")' % ("+" if rmlStyle else "--")) + defaultCustomCommands2.append('setCommandLineOptions("--simCodeTarget=Cpp")') else: defaultCustomCommands2 = defaultCustomCommands data["customCommands"] = (data.get("customCommands") or defaultCustomCommands2) + (data.get("extraCustomCommands") or []) + # A --simCodeTarget in the commands (e.g. from --extraflags) is what omc will + # actually use, so the rest of the testing scripts need to see it + data["simCodeTarget"] = simCodeTargetFromCommands(data["simCodeTarget"], data["customCommands"]) data["ulimitOmc"] = int(data.get("ulimitOmc") or 660) # 11 minutes to generate the C-code data["ulimitExe"] = int(data.get("ulimitExe") or 8*60) # 8 additional minutes to initialize and run the simulation - data["ulimitLoadModel"] = int(data.get("ulimitLoadModel") or 90) + data["ulimitLoadModel"] = int(data.get("ulimitLoadModel") or 3*60) # 3 minutes to load the files (could take a while if the ssd is doing backup) simflags = [] if data.get("extraSimFlags"): simflags.append(data.get("extraSimFlags")) if extrasimflags: simflags.append(extrasimflags) data["extraSimFlags"] = " ".join(simflags) # no extra sim flags + if data.get("environmentSimulation"): + data["environmentSimulation"] = data.get("environmentSimulation") + environmentSimulation + else: + data["environmentSimulation"] = environmentSimulation + if data.get("environmentTranslation"): + data["environmentTranslation"] = data.get("environmentTranslation") + environmentTranslation + else: + data["environmentTranslation"] = environmentTranslation data["libraryVersion"] = data.get("libraryVersion") or "default" data["libraryVersionLatestInPackageManager"] = data.get("libraryVersionLatestInPackageManager") or False - data["alarmFlag"] = data.get("alarmFlag") or (alarmFlag if data["simCodeTarget"]=="C" else "") + data["libraryVersionExactMatch"] = data.get("libraryVersionExactMatch") or False + data["alarmFlag"] = data.get("alarmFlag") or (alarmFlag if data["simCodeTarget"] in ("C","wasm-jit") else "") data["abortSlowSimulation"] = data.get("abortSlowSimulation") or (abortSimulationFlag if data["simCodeTarget"]=="C" else "") if "changeHash" in data: # Force rebuilding the library due to change in the testing script data["changeHash"] = data["changeHash"] return (data["library"],data) except: - print("Failed to fix data for: %s with extra args: %s" % (str(data),str((rmlStyle,abortSimulationFlag,alarmFlag,defaultCustomCommands)))) + print("Failed to fix data for: %s with extra args: %s" % (str(data),str((abortSimulationFlag,alarmFlag,defaultCustomCommands)))) raise -def readConfig(c,rmlStyle=False,abortSimulationFlag="",alarmFlag="",overrideDefaults=[],defaultCustomCommands=[],extrasimflags=""): - return [fixData(data,rmlStyle,abortSimulationFlag,alarmFlag,overrideDefaults,defaultCustomCommands,extrasimflags) for data in json.load(open(c))] +def readConfig(c,abortSimulationFlag="",alarmFlag="",overrideDefaults=[],defaultCustomCommands=[],extrasimflags="",environmentTranslation=[],environmentSimulation=[]): + return [fixData(data,abortSimulationFlag,alarmFlag,overrideDefaults,defaultCustomCommands,extrasimflags,environmentTranslation,environmentSimulation) for data in json.load(open(c))] def libname(library, conf): if "libraryVersionNameForTests" in conf: @@ -71,17 +93,119 @@ def getReferenceFileName(conf): referenceFile="" return referenceFile -def simulationAcceptsFlag(f, checkOutput=True, cwd=None): +def simulationAcceptsFlag(f, checkOutput=True, cwd=None, isWin=False): try: os.unlink("HelloWorld_res.mat") except OSError: pass try: - subprocess.check_output("./HelloWorld %s" % f, shell=True, stderr=subprocess.STDOUT, cwd=cwd) - if (not os.path.exists("HelloWorld_res.mat")): - print("Result file HelloWorld_res.mat WAS NOT generated running: ./HelloWorld with flags [%s]" % f) + if isWin: + subprocess.check_output("HelloWorld.bat %s" % f, shell=True, stderr=subprocess.STDOUT, cwd=cwd) + else: + subprocess.check_output("./HelloWorld %s" % f, shell=True, stderr=subprocess.STDOUT, cwd=cwd) + + #if (not os.path.exists("HelloWorld_res.mat")): + # print("Result file HelloWorld_res.mat WAS NOT generated running: ./HelloWorld with flags [%s]" % f) if (not checkOutput) or os.path.exists("HelloWorld_res.mat"): return True except subprocess.CalledProcessError as e: pass return False + +def isFMPy(fmisimulator): + if fmisimulator: + return 'fmpy' in fmisimulator + else: + return False + +def fmiSimulatorName(command): + """The name of the simulator a bare --fmisimulator runs. + + Any of the known names appearing in the command wins; a command that names + none of them is OMSimulator, which is how --fmisimulator was used before it + could name its simulator. + """ + for name in sorted(fmiSimulators(), key=len, reverse=True): + if name.lower() in command.lower(): + return name + return "OMSimulator" + +def parseFmiSimulators(fmisimulators): + """The --fmisimulator values as an ordered list of (name, command). + + A value is either "name=command" or just the command, whose name is then + taken from the command itself. The name decides which simulator of + configs/fmi-simulators.json is run and which branch its results go to, see + branchForSimulator. + """ + res = [] + for s in fmisimulators or []: + if not s: + continue + name, sep, command = s.partition("=") + if not sep or "/" in name or " " in name: + (name, command) = (fmiSimulatorName(s), s) + res.append((name, command)) + names = [n for (n, _) in res] + if len(set(names)) != len(names): + raise Exception("The same FMI simulator name is used twice: %s" % ", ".join(names)) + for name in names: + if fmiSimulator(name).get("untested"): + print("Warning: nobody has run %s through the testing yet; if its flags in %s are wrong, " + "every model will fail to simulate." % (name, FMI_SIMULATORS_FILE)) + return res + +# The FMI simulators live in configs/fmi-simulators.json, so that adding one is +# an entry in a file rather than a change to the scripts. A simulator stores +# its results in the branch of the job with its name appended, except +# OMSimulator, which has always had the plain -fmi table to itself; the mapping +# is keyed by the simulator and not by the order it was given in, so that a job +# running only FMPy still fills v1.27-fmi-fmpy and not v1.27-fmi. +FMI_SIMULATORS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "configs", "fmi-simulators.json") +_fmiSimulators = None + +def fmiSimulators(path=None): + """Everything the testing knows about the FMI simulators.""" + global _fmiSimulators + if _fmiSimulators is None or path: + with open(path or FMI_SIMULATORS_FILE) as fin: + _fmiSimulators = dict((k, v) for (k, v) in json.load(fin).items() if not k.startswith("_")) + return _fmiSimulators + +def fmiSimulator(name): + """What is known about an FMI simulator, by the name --fmisimulator gave it.""" + known = fmiSimulators() + if name not in known: + raise Exception("Unknown FMI simulator %s; known are %s. Adding one is an entry in %s." + % (name, ", ".join(sorted(known)), FMI_SIMULATORS_FILE)) + return known[name] + +def fmiSimulatorCommand(name, command, **values): + """The command line that runs one FMU with one simulator. + + arguments is a template over the values below plus anything the entry defines + in optionalArguments, which are the flags that have to disappear when there + is nothing to put in them: OMSimulator crashes on --stepSize=0 rather than + ignoring it, while FMPy wants --output-interval 0 all the same and therefore + writes the value straight into its arguments. + """ + spec = fmiSimulator(name) + values["simulator"] = command + for (key, template) in (spec.get("optionalArguments") or {}).items(): + used = [f.split(":")[0].split(".")[0].split("[")[0] + for (_, f, _, _) in string.Formatter().parse(template) if f] + values[key] = template.format(**values) if all(values.get(u) for u in used) else "" + return "%s %s" % (spec.get("command", "{simulator}").format(**values), + spec["arguments"].format(**values)) + +def branchForSimulator(branch, name): + """Where the results of one FMI simulator of a run are stored. + + --branch names the job, v1.27-fmi, and every simulator derives its own from + it: OMSimulator fills v1.27-fmi, FMPy v1.27-fmi-fmpy, whether they run + together or on their own. + """ + return branch + fmiSimulator(name).get("branchSuffix", "-%s" % name) + + diff --git a/single-model.py b/single-model.py index d72e769..3d9af2b 100755 --- a/single-model.py +++ b/single-model.py @@ -3,11 +3,12 @@ import sys, argparse import simplejson as json -import shared +import shared, resultsdb parser = argparse.ArgumentParser(description='OpenModelica model testing report generation tool') parser.add_argument('models', nargs='*') parser.add_argument('--branch', default='') +resultsdb.addArgument(parser) args = parser.parse_args() @@ -21,28 +22,28 @@ libs = {} -import cgi, sqlite3, time, datetime +import cgi, time, datetime from omcommon import friendlyStr, multiple_replace -conn = sqlite3.connect('sqlite3.db') -cursor = conn.cursor() +db = resultsdb.connect(args.db) +cursor = db.cursor() try: - cursor.execute("SELECT name FROM [sqlite_master] WHERE type='table' AND name=?", (branch,)) - v = cursor.fetchone()[0] + if not db.tableExists(branch): + raise Exception("no table") except: raise Exception("No such table '%s'; specify it using --branch=XXX" % branch) for model in models: lines=[] c=0 - libnames = [libname for (libname,) in cursor.execute("SELECT DISTINCT libname FROM [%s] WHERE model=? ORDER BY libname ASC" % (branch), (model,))] + libnames = [libname for (libname,) in cursor.execute("SELECT DISTINCT libname FROM %s WHERE model=? ORDER BY libname ASC" % (db.quote(branch)), (model,))] for libname in libnames: - for (finalphase,dint,libversion) in cursor.execute("SELECT finalphase,date,libversion FROM [%s] NATURAL JOIN [libversion] WHERE model=? AND libname=? ORDER BY date ASC" % (branch), (model,libname)): + for (finalphase,dint,libversion) in cursor.execute("SELECT finalphase,date,libversion FROM %s NATURAL JOIN libversion WHERE model=? AND libname=? ORDER BY date ASC" % (db.quote(branch)), (model,libname)): c+=1 dstr = str(datetime.datetime.fromtimestamp(dint).strftime('%Y-%m-%d %H:%M:%S')) - cursor2 = conn.cursor() - omcversion = cursor2.execute("SELECT omcversion FROM [omcversion] WHERE date=? AND branch=?", (dint,branch)).fetchone()[0] + cursor2 = db.cursor() + omcversion = cursor2.execute("SELECT omcversion FROM omcversion WHERE date=? AND branch=?", (dint,branch)).fetchone()[0] omcversion = omcversion.replace("OpenModelica ","").replace("OMCompiler ","") lines.insert(0, "%s %s %s %s" % (dstr,shared.finalphaseName(finalphase),omcversion,libversion.strip())) if c==0: diff --git a/sqlite2postgres.py b/sqlite2postgres.py new file mode 100644 index 0000000..af49bb9 --- /dev/null +++ b/sqlite2postgres.py @@ -0,0 +1,411 @@ +#!/usr/bin/env python3 +""" +Migrate OpenModelicaLibraryTesting sqlite3 result databases into PostgreSQL. + +The PostgreSQL layout is a 1:1 mirror of the sqlite3 one: one table per branch +with the same columns, plus the [omcversion] and [libversion] lookup tables. +Only the types are adapted (integer -> bigint, real -> double precision). + +Only the python standard library is used; the psql client binary does the +talking, so nothing has to be installed on the test machines. The password is +never passed on the command line - use PGPASSFILE or PGPASSWORD, as understood +by psql. + +Typical use: + + export PGPASSFILE=~/.pgpass + ./sqlite2postgres.py --sqlite dbs/ripper1.db --source ripper1 + ./sqlite2postgres.py --index + ./sqlite2postgres.py --sqlite dbs/ripper2.db --source ripper2 --skip-existing + ./sqlite2postgres.py --index + +The machines share the tables of the branches they both test, so they are merged +in that order: ripper1 first, then ripper2 with --skip-existing, which keeps the +row already in the database whenever a key collides. --index in between creates +the unique keys that decide what a collision is - (date, libname, model) for a +branch table - so it has to run before the second machine. + +--source only names the machine for the resume bookkeeping; it is not stored in +the data. Use --pgschema ripper1 to keep a machine in a schema of its own +instead of merging. + +The migration is resumable and safe to re-run: how far each table has been +copied is recorded in [migration_progress], and a restart continues from the +last sqlite rowid that made it in. +""" + +import argparse +import io +import os +import sqlite3 +import subprocess +import sys +import time + +# Columns of a per-branch table, in the order used by test.py. Databases with +# PRAGMA user_version < 3 have no "parsing" column. +# +# The sqlite3 tables declare every column NOT NULL, but tables created before +# that declaration still hold NULLs (libversion.libversion has some), so only +# the key columns are NOT NULL here. +BRANCH_COLUMNS = [ + ("date", "bigint"), + ("libname", "text"), + ("model", "text"), + ("exectime", "double precision"), + ("frontend", "double precision"), + ("backend", "double precision"), + ("simcode", "double precision"), + ("templates", "double precision"), + ("compile", "double precision"), + ("simulate", "double precision"), + ("verify", "double precision"), + ("verifyfail", "integer"), + ("verifytotal", "integer"), + ("finalphase", "integer"), + ("parsing", "double precision"), +] + +LOOKUP_COLUMNS = { + "omcversion": [("date", "bigint"), ("branch", "text"), ("omcversion", "text")], + "libversion": [("date", "bigint"), ("branch", "text"), ("libname", "text"), + ("libversion", "text"), ("confighash", "bigint"), + ("host", "text"), ("sysinfo", "text")], +} + +# Derived data, no longer generated by all-plots.py; not worth migrating. +SKIP_PREFIXES = ("datelookup_", "sqlite_") + + +# What identifies a row. A test run writes one row per model, so a model +# appears once per (date, libname); the lookup tables have one row per run and +# per (run, library). --index enforces these, which is what lets a second test +# machine be merged into a table that already holds another machine's results. +KEYS = { + "omcversion": ["date", "branch"], + "libversion": ["date", "branch", "libname", "confighash"], +} +BRANCH_KEY = ["date", "libname", "model"] + +PROGRESS_TABLE = """ +CREATE TABLE IF NOT EXISTS "migration_progress" ( + source text NOT NULL, + tbl text NOT NULL, + last_rowid bigint NOT NULL, + rows_read bigint NOT NULL, + done boolean NOT NULL DEFAULT false, + updated_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (source, tbl) +) +""" + + +def lit(s): + """Quote a python value as an SQL string literal.""" + return "'" + str(s).replace("'", "''") + "'" + + +def ident(s): + """Quote an SQL identifier. Branch names contain '-' and upper case.""" + return '"' + s.replace('"', '""') + '"' + + +def copy_text(v): + """Encode one value for COPY's text format. + + Not CSV: an empty CSV field reads back as NULL, and libversion.libversion + does contain empty strings that have to stay empty strings. The text format + spells NULL \\N instead, so both survive. + """ + if v is None: + return "\\N" + return (str(v).replace("\\", "\\\\").replace("\n", "\\n") + .replace("\r", "\\r").replace("\t", "\\t")) + + +class Psql: + """Runs SQL through the psql client, which keeps this script dependency-free.""" + + def __init__(self, args): + self.base = ["psql", "--no-psqlrc", "-w", "-v", "ON_ERROR_STOP=1", + "-h", args.host, "-p", str(args.port), "-U", args.user, "-d", args.dbname] + self.schema = args.pgschema + + def _run(self, argv, stdin=None): + p = subprocess.run(self.base + argv, input=stdin, capture_output=True, text=True) + if p.returncode != 0: + raise RuntimeError("psql failed: %s" % (p.stderr.strip() or p.stdout.strip())) + return p.stdout + + def query(self, sql): + """Execute SQL and return the output unaligned and without headers.""" + return self._run(["-tAq", "-c", "SET search_path TO %s; %s" % (ident(self.schema), sql)]).strip() + + def script(self, sql): + return self._run(["-q", "-f", "-"], stdin="SET search_path TO %s;\n%s" % (ident(self.schema), sql)) + + def copy_rows(self, table, columns, rows, also="", skip_existing=None): + """COPY an iterable of tuples into a table. + + The statements in "also" are committed together with the batch, so that a + migration killed halfway through never copies the same rows twice. + + With skip_existing set to the key columns of the table, the batch goes + through a temporary table first and rows that are already there are + dropped, so the machine migrated first keeps its results. + """ + cols = ",".join(ident(c) for c in columns) + target = ident(table) + buf = io.StringIO() + buf.write("SET search_path TO %s;\nBEGIN;\n" % ident(self.schema)) + if skip_existing: + buf.write("CREATE TEMP TABLE batch (LIKE %s) ON COMMIT DROP;\n" % target) + target = "batch" + buf.write("COPY %s (%s) FROM STDIN;\n" % (target, cols)) + for r in rows: + buf.write("\t".join(copy_text(v) for v in r)) + buf.write("\n") + buf.write("\\.\n") + if skip_existing: + key = ",".join(ident(c) for c in skip_existing) + # DISTINCT ON also drops rows the batch itself holds twice, which the + # unique index would otherwise reject. + buf.write("INSERT INTO %s (%s) SELECT DISTINCT ON (%s) %s FROM batch ORDER BY %s" + " ON CONFLICT DO NOTHING;\n" % (ident(table), cols, key, cols, key)) + if also: + buf.write(also.rstrip().rstrip(";") + ";\n") + buf.write("COMMIT;\n") + if not skip_existing: + self._run(["-q", "-f", "-"], stdin=buf.getvalue()) + return None + # Without -q psql reports every statement, so the INSERT says how many rows + # were new - the interesting number when catching up with a running test. + out = self._run(["-f", "-"], stdin=buf.getvalue()) + for line in out.split("\n"): + if line.startswith("INSERT "): + return int(line.split()[-1]) + return 0 + + +def sqlite_tables(conn): + """The tables of a testing database, split into (branch tables, lookup tables).""" + names = [n for (n,) in conn.execute( + "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")] + branches, lookups = [], [] + for n in names: + if any(n.startswith(p) for p in SKIP_PREFIXES): + continue + (lookups if n in LOOKUP_COLUMNS else branches).append(n) + return branches, lookups + + +def sqlite_columns(conn, tbl): + return [row[1] for row in conn.execute("PRAGMA table_info(%s)" % ident(tbl))] + + +def create_table(pg, tbl, columns): + key = KEYS.get(tbl, BRANCH_KEY) + cols = ",\n ".join("%s %s%s" % (ident(c), t, " NOT NULL" if c in key else "") + for c, t in columns) + pg.script("CREATE TABLE IF NOT EXISTS %s (\n %s\n);" % (ident(tbl), cols)) + + +def read_progress(pg, source, tbl): + out = pg.query("SELECT last_rowid, rows_read, done FROM migration_progress " + "WHERE source=%s AND tbl=%s" % (lit(source), lit(tbl))) + if not out: + return 0, 0, False + last, rows, done = out.split("|") + return int(last), int(rows), done == "t" + + +def progress_sql(source, tbl, last_rowid, rows_read, done): + return ("""INSERT INTO migration_progress (source, tbl, last_rowid, rows_read, done, updated_at) + VALUES (%s, %s, %d, %d, %s, now()) + ON CONFLICT (source, tbl) DO UPDATE SET + last_rowid = EXCLUDED.last_rowid, rows_read = EXCLUDED.rows_read, + done = EXCLUDED.done, updated_at = now()""" + % (lit(source), lit(tbl), last_rowid, rows_read, "true" if done else "false")) + + +def migrate_table(pg, sconn, source, tbl, columns, batch, quiet, skip_existing=False, + catch_up=False): + """Copy one sqlite table into its PostgreSQL twin, in resumable batches.""" + names = [c for c, _ in columns] + have = sqlite_columns(sconn, tbl) + missing = [c for c in names if c not in have] + # Older databases lack "parsing", and ones older still lack the libversion + # host columns. test.py defaults the numbers to 0; a text column a run never + # recorded is NULL, not the string "0". + types = dict(columns) + select = ",".join(ident(c) if c in have else ("NULL" if types[c] == "text" else "0") + for c in names) + + create_table(pg, tbl, columns) + last_rowid, rows_read, done = read_progress(pg, source, tbl) + if done and not catch_up: + if not quiet: + print(" %-30s done earlier (%d rows)" % (tbl, rows_read)) + return rows_read, 0 + + stored = None + if catch_up: + # Read the table from the start and keep the runs the database has never + # seen. Not "everything past the rowid we stopped at": VACUUM renumbers + # the rowids of these tables, and clean-empty-omcversion-dates.py runs one + # after every test, so that number cannot be trusted between two runs. + stored = set(int(d) for d in pg.query("SELECT DISTINCT date FROM %s" % ident(tbl)).split("\n") if d) + last_rowid, rows_read = 0, 0 + skip_existing = True + key = KEYS.get(tbl, BRANCH_KEY) if skip_existing else None + if not quiet: + extra = "" + if missing: + extra = ", %s defaulted to 0" % ",".join(missing) + if catch_up: + extra += ", %d runs already stored" % len(stored) + elif last_rowid: + extra += ", resuming after rowid %d" % last_rowid + print(" %-30s starting%s" % (tbl, extra)) + + t0 = time.time() + inserted = 0 + date = names.index("date") + while True: + chunk = sconn.execute( + "SELECT rowid,%s FROM %s WHERE rowid > ? ORDER BY rowid LIMIT %d" + % (select, ident(tbl), batch), (last_rowid,)).fetchall() + if not chunk: + break + last_rowid = chunk[-1][0] + rows_read += len(chunk) + rows = [row[1:] for row in chunk] + if stored is not None: + rows = [r for r in rows if r[date] not in stored] + if rows: + n = pg.copy_rows(tbl, names, rows, + also=progress_sql(source, tbl, last_rowid, rows_read, False), + skip_existing=key) + inserted += n if n is not None else len(rows) + if not quiet: + sys.stdout.write("\r %-30s %d rows read, %d new (%.0f rows/s)" + % (tbl, rows_read, inserted, rows_read / max(time.time() - t0, 1e-9))) + sys.stdout.flush() + + pg.query(progress_sql(source, tbl, last_rowid, rows_read, True)) + if not quiet and (inserted or not catch_up): + print("\r %-30s %d rows read, %d new%s" % (tbl, rows_read, inserted, " " * 30)) + return rows_read, inserted + + +def migrate_database(pg, args): + sconn = sqlite3.connect("file:%s?mode=ro" % os.path.abspath(args.sqlite), uri=True) + branches, lookups = sqlite_tables(sconn) + if args.only: + branches = [b for b in branches if b in args.only] + lookups = [l for l in lookups if l in args.only] + print("%s: %d branch tables, lookup tables: %s" + % (args.sqlite, len(branches), ",".join(lookups) or "none")) + + total = new = 0 + for tbl in lookups + branches: + cols = LOOKUP_COLUMNS.get(tbl, BRANCH_COLUMNS) + (r, i) = migrate_table(pg, sconn, args.source, tbl, cols, args.batch, args.quiet, + args.skip_existing, args.catch_up) + total += r + new += i + sconn.close() + print("%s: %d rows read, %d new" % (args.sqlite, total, new)) + + +def create_indexes(pg): + """The unique key of each table, plus the index the reports need. + + The unique index has to exist before a second test machine is migrated with + --skip-existing. It also covers the [date] index the report scripts create + on the fly in sqlite, since date is its first column. + """ + tables = [t for t in pg.query( + "SELECT tablename FROM pg_tables WHERE schemaname=current_schema() ORDER BY tablename").split("\n") if t] + + def index(tbl, cols, unique=False): + name = ("%s_%s_%s" % ("uq" if unique else "idx", tbl, "_".join(cols)))[:63] + pg.script("CREATE %sINDEX IF NOT EXISTS %s ON %s (%s);" + % ("UNIQUE " if unique else "", ident(name), ident(tbl), + ",".join(ident(c) for c in cols))) + + for tbl in tables: + if tbl == "migration_progress": + continue + index(tbl, KEYS.get(tbl, BRANCH_KEY), unique=True) + if tbl == "omcversion": + index(tbl, ["branch", "date"]) + elif tbl == "libversion": + index(tbl, ["branch", "libname", "date"]) + else: + index(tbl, ["libname", "date"]) + print(" indexed %s" % tbl) + + +def verify(pg, args): + """Compare the sqlite and PostgreSQL row counts table by table.""" + sconn = sqlite3.connect("file:%s?mode=ro" % os.path.abspath(args.sqlite), uri=True) + branches, lookups = sqlite_tables(sconn) + bad = 0 + for tbl in lookups + branches: + (n,) = sconn.execute("SELECT COUNT(*) FROM %s" % ident(tbl)).fetchone() + m = int(pg.query("SELECT COUNT(*) FROM %s" % ident(tbl)) or 0) + flag = "ok" if m >= n else "MISSING" + if m < n: + bad += 1 + print(" %-30s sqlite %10d postgres %10d %s" % (tbl, n, m, flag)) + sconn.close() + print("%d of %d tables incomplete" % (bad, len(lookups) + len(branches))) + return bad + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--sqlite", help="sqlite3 database to migrate") + parser.add_argument("--source", help="machine the database comes from, e.g. ripper1; used for resuming") + parser.add_argument("--host", default=os.environ.get("PGHOST", "openmodelica.org")) + parser.add_argument("--port", type=int, default=int(os.environ.get("PGPORT", 5432))) + parser.add_argument("--user", default=os.environ.get("PGUSER", "om")) + parser.add_argument("--dbname", default=os.environ.get("PGDATABASE", "omdb")) + parser.add_argument("--pgschema", default="public", help="PostgreSQL schema to write to (default public)") + parser.add_argument("--batch", type=int, default=200000, help="rows per COPY batch (default 200000)") + parser.add_argument("--only", action="append", help="migrate only this table (repeatable)") + parser.add_argument("--catch-up", action="store_true", + help="copy the runs written since the last migration, for a test that was " + "still using the sqlite3 database. Reads the whole database but only " + "writes the runs it does not have; safe to repeat") + parser.add_argument("--skip-existing", action="store_true", + help="keep the rows already in the database when a key collides; " + "use it for every machine after the first one, and run --index before") + parser.add_argument("--index", action="store_true", help="create the indexes; do this after loading") + parser.add_argument("--verify", action="store_true", help="compare row counts with --sqlite") + parser.add_argument("--quiet", action="store_true") + args = parser.parse_args() + + pg = Psql(args) + pg.script("CREATE SCHEMA IF NOT EXISTS %s;" % ident(args.pgschema)) + pg.script(PROGRESS_TABLE) + + if args.sqlite and not args.verify: + if not args.source: + parser.error("--sqlite needs --source, the machine name, e.g. ripper1") + migrate_database(pg, args) + + if args.index: + print("creating indexes") + create_indexes(pg) + + if args.verify: + if not args.sqlite: + parser.error("--verify needs --sqlite") + sys.exit(1 if verify(pg, args) else 0) + + +if __name__ == "__main__": + main() diff --git a/test.py b/test.py index b7278cb..8351f66 100755 --- a/test.py +++ b/test.py @@ -4,43 +4,211 @@ # TODO: When libraries hash changes, run with the old OMC against the new libs # Then run with the new OMC against the new libs -import html, shutil, sys, os, re, glob, time, argparse, sqlite3, datetime, math, platform +import sys +if (sys.version_info < (3, 0)): + raise Exception("Python2 is no longer supported") + +import html, shutil, os, re, glob, time, argparse, datetime, math, platform from joblib import Parallel, delayed import simplejson as json import psutil, subprocess, threading, hashlib +import socket from subprocess import call from monotonic import monotonic from omcommon import friendlyStr, multiple_replace from natsort import natsorted -from shared import readConfig, getReferenceFileName, simulationAcceptsFlag -import shared +from shared import readConfig, getReferenceFileName, simulationAcceptsFlag, isFMPy +from platform import processor +import shared, resultsdb import signal -if (sys.version_info > (3, 0)): - basestring = (str, bytes) +parser = argparse.ArgumentParser(description='OpenModelica library testing tool') +parser.add_argument('configs', nargs='*') +parser.add_argument('--branch', default='master') +parser.add_argument('--fmi', default=False) +parser.add_argument('--fmuType', default='me', help="FMU type: 'me' for Model Exchange, 'cs' for Co-Simulation.") +parser.add_argument('--output', default='') +parser.add_argument('--docker', default='') +parser.add_argument('--libraries', help="Directory omc will search in to load system libraries/libraries to test.", default='') +parser.add_argument('--extraflags', default='') +parser.add_argument('--extrasimflags', default='') +parser.add_argument('--ompython_omhome', default='') +parser.add_argument('--noclean', action="store_true", default=False) +parser.add_argument('--nobuildmodel', action="store_true", help="Translate, build and simulate in a single simulate() call instead of translateModel() followed by simulate(resimulateExecutable=...), so the JIT compile is reported as build time rather than simulation time. Only used by simCodeTarget=wasm-jit.", default=False) +parser.add_argument('--coldhot', action="store_true", help="Simulate each model twice in the same omc; the second run reuses the compiled module. Both times are printed, but only the hot one is stored. Only used by simCodeTarget=wasm-jit.", default=False) +parser.add_argument('--fmisimulator', action='append', default=[], help="FMI simulator to run the FMUs with, as 'name=command' or just the command. Repeat it to simulate every FMU with several tools without building it more than once; the first one stores its results in --branch and each further one in -, so --branch=master-fmi with OMSimulator and fmpy fills master-fmi and master-fmi-fmpy." ) +parser.add_argument('--ulimitvmem', help="Virtual memory limit (in kB) (linux only)", type=int, default=8*1024*1024) +parser.add_argument('--default', action='append', help="Add a default value for some configuration key, such as --default=ulimitExe=60. The equals sign is mandatory.", default=[]) +parser.add_argument('-j', '--jobs', default=0, help="Ignored and deprecated, use procOMC:0 or procOMC:1 in the config") +parser.add_argument('-v', '--verbose', action="store_true", help="Verbose mode.", default=False) +parser.add_argument('--execAllTests', action="store_true", help="Force all tests to be executed", default=False) +parser.add_argument('--noSync', action="store_true", help="Move files using python instead of rsync", default=False) +parser.add_argument('--timeout', default=0, help="=[value] timeout in seconds for each test, it overrides the timeout calculated by the script") +parser.add_argument('--msysEnvironment', help = 'MSYS2 environment used by OpenModelica on Windows.', default = 'ucrt64') +parser.add_argument('--debug', action="store_true", help="turn on the DEBUG mode", default=False) +parser.add_argument('--addmsl', action="store_true", help="add the MSL path to the OPENMODELICAPATH if the MSL is not detected in the libraries path", default=False) +resultsdb.addArgument(parser) + +args = parser.parse_args() +configs = args.configs +branch = args.branch +noSync = args.noSync +isWin = os.name == 'nt' +DEBUG = args.debug or False # set this to True for debug output + +# result location can be on a remote server, do NOT use os.path.abspath on it if the system is not Windows or if --noSync=True +result_location = '' +if not isWin and not noSync: + result_location = args.output if args.output else '' +else: + result_location = os.path.abspath(args.output) if args.output else '' +n_jobs = int(args.jobs) +clean = not args.noclean +runverbose = args.verbose +extraflags = args.extraflags +extrasimflags = args.extrasimflags +ompython_omhome = args.ompython_omhome +fmisimulators = shared.parseFmiSimulators(args.fmisimulator) +# The first simulator is the one the single-simulator code paths use. +fmisimulator = fmisimulators[0][1] if fmisimulators else None + +# One branch per simulator. The first reports itself in the results of the +# model and the others under their own name, but every one of them stores its +# results where its own simulator belongs: a job given only FMPy on +# --branch=v1.27-fmi fills v1.27-fmi-fmpy, not v1.27-fmi. +resultBranches = [(branch, None)] +if fmisimulators: + resultBranches = [(shared.branchForSimulator(branch, fmisimulators[0][0]), None)] + for (simulatorName, _) in fmisimulators[1:]: + resultBranches.append((shared.branchForSimulator(branch, simulatorName), simulatorName)) +# What the run asks about when it looks for results it already has. +primaryBranch = resultBranches[0][0] + +jobOutput = result_location + +def outputFor(resultBranch): + """Where a branch publishes. + + --output names the directory of the job, which ends in the branch the job was + started with, so every simulator gets the directory of its own branch beside + it: a job given only FMPy on --branch=v1.27-fmi publishes v1.27-fmi-fmpy, the + same branch its results are stored under. + + rsync runs from the directory a branch is staged in, so a local destination + has to be absolute; a remote one, host:path, already is. + """ + location = jobOutput + if jobOutput: + base = jobOutput.rstrip("/") + if base.endswith("/" + branch): + location = "%s%s" % (base[:-len(branch)], resultBranch) + elif resultBranch != primaryBranch: + location = "%s/%s" % (base, resultBranch) + if location and ":" not in location.split("/")[0]: + location = os.path.abspath(location) + return location + + +allTestsFmi = args.fmi +fmuType = args.fmuType +ulimitMemory = args.ulimitvmem +docker = args.docker +addmsl = args.addmsl + +numberOfTests = 0 + +if args.libraries: + librariespath = os.path.abspath(os.path.normpath(args.libraries)) +else: + if isWin: + librariespath = os.path.normpath(os.path.join(os.environ.get('APPDATA'), '.openmodelica', 'libraries')) + else: + librariespath = os.path.normpath(os.path.join(os.environ.get('HOME'), '.openmodelica', 'libraries')) + +# add the MSL path if mslpath=True and the MSL is not found in the libraries path +MSLpath = '' +if addmsl and len(glob.glob('Modelica *', root_dir=librariespath)) == 0: + if isWin: + MSLpath = ';' + os.path.normpath(os.path.join(os.environ.get('APPDATA'), '.openmodelica', 'libraries')).replace('\\','/') + else: + MSLpath = ':' + os.path.normpath(os.path.join(os.environ.get('HOME'), '.openmodelica', 'libraries')) + +overrideDefaults = [arg.split("=", 1) for arg in args.default] +execAllTests = args.execAllTests +msysEnvironment = args.msysEnvironment + +exeExt = ".exe" if isWin else "" +customTimeout = int(args.timeout) + +pythonExecutable = sys.executable +if not pythonExecutable: + pythonExecutable = "python" + +# adds quotes to sorround path elements that contain blank characters - to be used only for Popen calls under Windows +pythonExecutablePopenWin = os.path.join(*['\"'+i+'\"' if ' ' in i else i+'\\' if ':' in i else i for i in pythonExecutable.split('\\')]) if isWin else '' + +def fflush(): + sys.stdout.flush() + sys.stderr.flush() + +def check_output_log(*popenargs, **kwargs): + if DEBUG: + print("run: check_output", popenargs, kwargs) + print("\n") + fflush() + return subprocess.check_output(*popenargs, **kwargs) + +def check_call_log(*popenargs, **kwargs): + if DEBUG: + print("run: check_call", popenargs, kwargs) + print("\n") + fflush() + return subprocess.check_call(*popenargs, **kwargs) def rmtree(f): try: shutil.rmtree(f) except UnicodeDecodeError: # Yes, we can get UnicodeDecodeError because shutil.rmtree is poorly implemented - subprocess.check_call(["rm", "-rf", f], stderr=subprocess.STDOUT) + check_call_log(["rm", "-rf", f], stderr=subprocess.STDOUT) def print_linenum(signum, frame): - print("Currently at line", frame.f_lineno) + print("Currently at line", frame.f_lineno) -signal.signal(signal.SIGUSR1, print_linenum) +if not isWin: + signal.signal(signal.SIGUSR1, print_linenum) + +if isWin or noSync: + # delete temporary ./files to avoid moving of old results (win does not use rsync) + try: + rmtree("./files") + print("Cleaned files directory") + except OSError: + pass + try: + os.mkdir("files") + except OSError: + pass + print("Created files directory") + sys.stdout.flush() def runCommand(cmd, prefix, timeout): process = [None] def target(): + if DEBUG: + print("run: Popen %s, %s, %d\n" %(cmd, prefix, timeout)) + fflush() with open(os.devnull, 'w') as FNULL: - process[0] = subprocess.Popen(cmd, shell=True, stdin=FNULL, stdout=FNULL, stderr=FNULL, preexec_fn=os.setpgrp) + if isWin: + process[0] = subprocess.Popen(cmd, shell=True, stdin=FNULL, stdout=FNULL, stderr=FNULL) + else: + process[0] = subprocess.Popen(cmd, shell=True, stdin=FNULL, stdout=FNULL, stderr=FNULL, preexec_fn=os.setpgrp) + while process[0].poll() is None: process[0].communicate(1) process[0].wait(1) - + thread = threading.Thread(target=target) thread.start() @@ -50,10 +218,16 @@ def target(): if thread.is_alive(): gotTimeout = True - os.kill(-process[0].pid, signal.SIGTERM) + if isWin: + os.kill(process[0].pid, signal.SIGTERM) + else: + os.kill(-process[0].pid, signal.SIGTERM) thread.join(min(10, timeout)) if thread.is_alive(): - os.kill(-process[0].pid, signal.SIGKILL) + if isWin: + os.kill(process[0].pid, signal.SIGKILL) + else: + os.kill(-process[0].pid, signal.SIGKILL) thread.join() if clean: @@ -80,47 +254,19 @@ def target(): return 1 if gotTimeout else process[0].returncode try: - subprocess.check_output(["./testmodel.py", "--help"], stderr=subprocess.STDOUT) + check_output_log([pythonExecutable, "testmodel.py", "--help"], stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: - print("Sanity check failed (./testmodel.py --help):\n" + e.output) + print("Sanity check failed (./testmodel.py --help):\n" + e.output.decode()) sys.exit(1) -parser = argparse.ArgumentParser(description='OpenModelica library testing tool') -parser.add_argument('configs', nargs='*') -parser.add_argument('--branch', default='master') -parser.add_argument('--fmi', default=False) -parser.add_argument('--output', default='') -parser.add_argument('--docker', default='') -parser.add_argument('--libraries', default='') -parser.add_argument('--extraflags', default='') -parser.add_argument('--extrasimflags', default='') -parser.add_argument('--ompython_omhome', default='') -parser.add_argument('--noclean', action="store_true", default=False) -parser.add_argument('--rml', action="store_true", default=False) -parser.add_argument('--corba', action="store_true", default=False) -parser.add_argument('--fmisimulator', default='') -parser.add_argument('--ulimitvmem', help="Virtual memory limit (in kB)", type=int, default=8*1024*1024) -parser.add_argument('--default', action='append', help="Add a default value for some configuration key, such as --default=ulimitExe=60. The equals sign is mandatory.", default=[]) -parser.add_argument('-n', default=psutil.cpu_count(logical=False)) +# how many jobs in parallel? +n_cores = psutil.cpu_count(logical=False) +if n_jobs == 0: + n_jobs = n_cores -args = parser.parse_args() -configs = args.configs -branch = args.branch -result_location = args.output -n_jobs = int(args.n) -clean = not args.noclean -extraflags = args.extraflags -extrasimflags = args.extrasimflags -ompython_omhome = args.ompython_omhome -fmisimulator = args.fmisimulator or None -allTestsFmi = args.fmi -ulimitMemory = args.ulimitvmem -docker = args.docker -librariespath = args.libraries -rmlStyle = args.rml -corbaStyle = args.corba -overrideDefaults = [arg.split("=", 1) for arg in args.default] print("branch: %s, n_jobs: %d" % (branch, n_jobs)) + if clean: print("Removing temporary files, etc to the best ability of the script") @@ -134,8 +280,8 @@ def target(): print("Warning: Using docker to run this will fail. It is not fully working and needs to be reworked to use docker exec instead of docker run") print("###") print("###") - dir_path = os.path.dirname(os.path.realpath(__file__)) - subprocess.check_output(["docker", "pull", docker], stderr=subprocess.STDOUT).strip() + dir_path = os.path.normpath(os.path.dirname(os.path.realpath(__file__))) + check_output_log(["docker", "pull", docker], stderr=subprocess.STDOUT).strip() dockerExtraArgs = ["-w", dir_path, "-v", "%s:%s" % (dir_path,dir_path), "--env", "OPENMODELICALIBRARY=%s" % "/usr/lib/omlibrary", "--env", "GC_MARKERS=1", "-v", "%s:/usr/lib/omlibrary" % librariespath] commands = ["docker", "run", "--user", str(os.getuid())] + dockerExtraArgs + [docker] omc_cmd = commands + ["omc"] @@ -143,11 +289,11 @@ def target(): commands = [] dockerExtraArgs = [] if os.environ.get("OPENMODELICAHOME"): - omc_cmd = ["%s/bin/omc" % os.environ.get("OPENMODELICAHOME")] + omc_cmd = [os.path.normpath(os.path.join(os.environ.get("OPENMODELICAHOME"), 'bin', 'omc'))] else: omc_cmd = ["omc"] -if result_location != "" and not os.path.exists(result_location): - os.makedirs(result_location) +if result_location != "" and not os.path.exists(outputFor(primaryBranch)): + os.makedirs(outputFor(primaryBranch)) if configs == []: print("Error: Expected at least one configuration file to start the library test") @@ -155,12 +301,6 @@ def target(): from OMPython import OMCSession, OMCSessionZMQ -version_cmd = "--version" -single_thread="-n=1" -rmlStyle=False - - - # Try to make the processes a bit nicer... os.environ["GC_MARKERS"]="1" @@ -169,18 +309,13 @@ def target(): if ompython_omhome != "": # Use a different OMC for running OMPython than for running the tests omhome = os.environ["OPENMODELICAHOME"] - if rmlStyle: - omc_version = subprocess.check_output(omc_cmd + ["+version"], stderr=subprocess.STDOUT).decode("ascii").strip() - version_cmd = "+version" - print("Work-around for RML-style command-line arguments (+version)") - else: - omc_version = subprocess.check_output(omc_cmd + ["--version"], stderr=subprocess.STDOUT).decode("ascii").strip() + omc_version = check_output_log(omc_cmd + ["--version"], stderr=subprocess.STDOUT).decode("ascii").strip() os.environ["OPENMODELICAHOME"] = ompython_omhome omc = OMCSessionZMQ() ompython_omc_version=omc.sendExpression('getVersion()') os.environ["OPENMODELICAHOME"] = omhome else: - omc = OMCSession(docker=docker, dockerExtraArgs=dockerExtraArgs) if corbaStyle else OMCSessionZMQ(docker=docker, dockerExtraArgs=dockerExtraArgs) + omc = OMCSessionZMQ(docker=docker, dockerExtraArgs=dockerExtraArgs) omhome=omc.sendExpression('getInstallationDirectoryPath()') omc_version=omc.sendExpression('getVersion()') ompython_omc_version=omc_version @@ -190,30 +325,40 @@ def timeSeconds(f): return html.escape("%.2f" % f) if not docker: - omc.sendExpression('setModelicaPath("%s")' % librariespath) -omc_exe=os.path.join(omhome,"bin","omc") -dygraphs=os.path.join(ompython_omhome or omhome,"share","doc","omc","testmodels","dygraph-combined.js") + omc.sendExpression('setModelicaPath("%s")' % (librariespath.replace('\\','/') + MSLpath,)) +omc_exe=os.path.normpath(os.path.join(omhome,"bin","omc")) +dygraphs="" +for p in os.path.normpath(os.path.join(ompython_omhome or omhome,"share","doc","omc","testmodels","dygraph-combined.js")): + if os.path.exists(dygraphs): # docker images strip /usr/share/doc from the image... + dygraphs = p print(omc_exe,omc_version,dygraphs) sys.stdout.flush() # Do feature checks. Handle things like old RML-style arguments... -try: - subprocess.check_output(omc_cmd + ["-n=1", version_cmd], stderr=subprocess.STDOUT).strip() - single_thread="-n=1" -except: - subprocess.check_output(omc_cmd + ["+n=1", version_cmd], stderr=subprocess.STDOUT).strip() - single_thread="+n=1" - rmlStyle=True - print("Work-around for RML-style command-line arguments (+n=1)") +check_output_log(omc_cmd + ["-n=1", "--version"], stderr=subprocess.STDOUT).strip() sys.stdout.flush() +def fmiSimulatorVersion(command): + try: + if not isFMPy(command): + return check_output_log([command, "-v"], stderr=subprocess.STDOUT).strip() + return subprocess.getoutput(command + " --version").strip().encode('ascii') + except subprocess.CalledProcessError as e: + print("Failure to run %s:\n%s" % (command, e.output)) + raise e + +fmisimulatorversions = {} fmisimulatorversion = None if fmisimulator: - fmisimulatorversion = subprocess.check_output([fmisimulator, "-v"], stderr=subprocess.STDOUT).strip() - print(fmisimulatorversion) + for (name, command) in fmisimulators: + fmisimulatorversions[name] = fmiSimulatorVersion(command) + print("%s: %s" % (name, fmisimulatorversions[name])) + # The version of the first one goes into the library version of every branch, + # as it did when a job ran a single simulator. + fmisimulatorversion = fmisimulatorversions[fmisimulators[0][0]] else: if allTestsFmi: raise Exception("No OMSimulator; trying to simulate using FMI") @@ -222,13 +367,13 @@ def timeSeconds(f): sys.stdout.flush() try: - os.unlink("HelloWorld") + os.unlink("HelloWorld"+exeExt) except OSError: pass -subprocess.check_output(omc_cmd + ["%ssimCodeTarget=Cpp" % ("+" if rmlStyle else "--"), "HelloWorld.mos"], stderr=subprocess.STDOUT) -if os.path.exists("HelloWorld"): +check_output_log(omc_cmd + ["--simCodeTarget=Cpp", "HelloWorld.mos"], stderr=subprocess.STDOUT) +if os.path.exists("HelloWorld"+exeExt): print("Have C++ HelloWorld simulation executable") - haveCppRuntime=simulationAcceptsFlag("") + haveCppRuntime=simulationAcceptsFlag("", isWin=isWin) if haveCppRuntime: print("Have C++ runtime") else: @@ -240,10 +385,10 @@ def timeSeconds(f): sys.stdout.flush() try: - subprocess.check_output(omc_cmd + ["Architecture.mo"], stderr=subprocess.STDOUT) + check_output_log(omc_cmd + ["Architecture.mo"], stderr=subprocess.STDOUT) except subprocess.CalledProcessError: print("Patching ModelicaServices for Architecture bug...") - for f in glob.glob(librariespath + "/ModelicaServices*/package.mo") + glob.glob(omhome + "/Modelica */Constants.mo"): + for f in glob.glob(librariespath + os.path.normpath("/ModelicaServices*/package.mo")) + glob.glob(omhome + os.path.normpath("/Modelica */Constants.mo")): with open(f) as fin: content = fin.read() assert(len(content) > 0) @@ -258,31 +403,30 @@ def timeSeconds(f): defaultCustomCommands = [] if extraflags: defaultCustomCommands += [extraflags] -debug = "+d" if rmlStyle else "-d" def testHelloWorld(cmd): with open("HelloWorld.mos") as fin: helloWorldContents = fin.read() try: - os.unlink("HelloWorld") + os.unlink("HelloWorld"+exeExt) except OSError: pass open("HelloWorld.cmd.mos","w").write(cmd + "\n" + helloWorldContents) try: - out=subprocess.check_output(omc_cmd + ["HelloWorld.cmd.mos"], stderr=subprocess.STDOUT) - if os.path.exists("HelloWorld") and not "Error:" in out.decode(): + out=check_output_log(omc_cmd + ["HelloWorld.cmd.mos"], stderr=subprocess.STDOUT) + if os.path.exists("HelloWorld"+exeExt) and not "Error:" in out.decode(): return True except subprocess.CalledProcessError as e: pass return False for cmd in [ - 'setCommandLineOptions("%s=nogen");' % debug, - 'setCommandLineOptions("%s=initialization");' % debug, - 'setCommandLineOptions("%s=backenddaeinfo");' % debug, - 'setCommandLineOptions("%s=discreteinfo");' % debug, - 'setCommandLineOptions("%s=stateselection");' % debug, - 'setCommandLineOptions("%s=execstat");' % debug, + 'setCommandLineOptions("-d=nogen");', + 'setCommandLineOptions("-d=initialization");', + 'setCommandLineOptions("-d=backenddaeinfo");', + 'setCommandLineOptions("-d=discreteinfo");', + 'setCommandLineOptions("-d=stateselection");', + 'setCommandLineOptions("-d=execstat");', 'setMatchingAlgorithm("PFPlusExt");', 'setIndexReductionMethod("dynamicStateSelection");' ]: @@ -297,35 +441,36 @@ def testHelloWorld(cmd): fmiOK_C = False fmiOK_Cpp = False -try: - out=subprocess.check_output(omc_cmd + ["--simCodeTarget=C", "FMI.mos"], stderr=subprocess.STDOUT) - if os.path.exists("HelloWorldX.fmu") and not "Error:" in out.decode(): - fmiOK_C = True - print("C FMU OK") - else: - print("No C-based FMUs (files not generated in correct location)") -except subprocess.CalledProcessError as e: - pass -try: - out=subprocess.check_output(omc_cmd + ["--simCodeTarget=Cpp", "FMI.mos"], stderr=subprocess.STDOUT) - if os.path.exists("HelloWorldX.fmu") and not "Error:" in out.decode(): - fmiOK_Cpp = True - print("C++ FMU OK") - else: - print("No C++-based FMUs (files not generated in correct location)") -except subprocess.CalledProcessError as e: - pass +if allTestsFmi: + try: + out=check_output_log(omc_cmd + ["--simCodeTarget=C", "FMI.mos"], stderr=subprocess.STDOUT) + if os.path.exists("HelloWorldX.fmu") and not "Error:" in out.decode(): + fmiOK_C = True + print("C FMU OK") + else: + print("No C-based FMUs (files not generated in correct location)") + except subprocess.CalledProcessError as e: + pass + try: + out=check_output_log(omc_cmd + ["--simCodeTarget=Cpp", "FMI.mos"], stderr=subprocess.STDOUT) + if os.path.exists("HelloWorldX.fmu") and not "Error:" in out.decode(): + fmiOK_Cpp = True + print("C++ FMU OK") + else: + print("No C++-based FMUs (files not generated in correct location)") + except subprocess.CalledProcessError as e: + pass try: - os.unlink("HelloWorld") + os.unlink("HelloWorld"+exeExt) except OSError: pass -print(subprocess.check_output(omc_cmd + ["HelloWorld.mos"], stderr=subprocess.STDOUT).decode().strip()) -assert(os.path.exists("HelloWorld")) -abortSimulationFlag="-abortSlowSimulation" if simulationAcceptsFlag("-abortSlowSimulation") else "" -alarmFlag="-alarm" if simulationAcceptsFlag("-alarm=480") else "" +print(check_output_log(omc_cmd + ["HelloWorld.mos"], stderr=subprocess.STDOUT).decode().strip()) +assert(os.path.exists("HelloWorld"+exeExt)) +abortSimulationFlag="-abortSlowSimulation" if simulationAcceptsFlag("-abortSlowSimulation", isWin=isWin) else "" +alarmFlag="-alarm" if simulationAcceptsFlag("-alarm=480", isWin=isWin) else "" -configs_lst = [readConfig(c, rmlStyle=rmlStyle, abortSimulationFlag=abortSimulationFlag, alarmFlag=alarmFlag, overrideDefaults=overrideDefaults, defaultCustomCommands=defaultCustomCommands, extrasimflags=extrasimflags) for c in configs] +configs_lst = [readConfig(c, abortSimulationFlag=abortSimulationFlag, alarmFlag=alarmFlag, overrideDefaults=overrideDefaults, defaultCustomCommands=defaultCustomCommands, extrasimflags=extrasimflags) for c in configs] configs = [] preparedReferenceDirs = {} for c in configs_lst: @@ -333,33 +478,78 @@ def testHelloWorld(cmd): for (lib,c) in configs: if "referenceFiles" in c: c["referenceFilesURL"] = c["referenceFiles"] - if isinstance(c["referenceFiles"], basestring): - m = re.search("^[$][A-Z]+", c["referenceFiles"]) + if isinstance(c["referenceFiles"], (str, bytes)): + m = re.search("^[$][A-Z_]+", c["referenceFiles"]) if m: k = m.group(0)[1:] if k not in os.environ: raise Exception("Environment variable %s not defined, but used in JSON config for reference files" % k) c["referenceFiles"] = c["referenceFiles"].replace(m.group(0), os.environ[k]) elif "giturl" in c["referenceFiles"]: - if c["referenceFiles"]["destination"] in preparedReferenceDirs: + refFilesGitTag = "origin/master" + if "git-ref" in c["referenceFiles"]: + refFilesGitTag = c["referenceFiles"]["git-ref"].strip() + if not refFilesGitTag.startswith("origin/"): + refFilesGitTag = 'origin/%s' % refFilesGitTag + # Several libraries share one set of reference files. The first of them + # prepares the directory and the rest take what it recorded - but the rest + # of this loop still has to run for every one of them. + destination = os.path.normpath(c["referenceFiles"]["destination"]) + if destination in preparedReferenceDirs: (c["referenceFiles"],c["referenceFilesURL"]) = preparedReferenceDirs[destination] - continue - giturl = c["referenceFiles"]["giturl"] - destination = c["referenceFiles"]["destination"] - if not os.path.isdir(destination): - subprocess.check_call(["git", "clone", giturl, destination], stderr=subprocess.STDOUT) - destinationReal = os.path.realpath(destination) - subprocess.check_call(["git", "clean", "-fdx", "--exclude=*.hash"], stderr=subprocess.STDOUT, cwd=destinationReal) - subprocess.check_call(["git", "fetch", "origin"], stderr=subprocess.STDOUT, cwd=destination) - subprocess.check_call(["git", "reset", "--hard", "origin/master"], stderr=subprocess.STDOUT, cwd=destinationReal) - subprocess.check_call(["git", "clean", "-fdx", "--exclude=*.hash"], stderr=subprocess.STDOUT, cwd=destinationReal) - githash = subprocess.check_output(["git", "rev-parse", "--verify", "HEAD"], stderr=subprocess.STDOUT, cwd=destinationReal) - c["referenceFiles"] = destinationReal - if giturl.startswith("https://github.com"): - c["referenceFilesURL"] = '%s (%s)' % (giturl,githash.strip(),giturl,githash.strip()) else: - c["referenceFilesURL"] = "%s (%s)" % (giturl,githash.strip()) - preparedReferenceDirs[destination] = (c["referenceFiles"],c["referenceFilesURL"]) + giturl = c["referenceFiles"]["giturl"] + if DEBUG: + print("destination %s" % destination) + print("not os.path.isdir(destination): %s" % (not os.path.isdir(destination))) + destinationReal = os.path.realpath(destination) + if DEBUG: + print("destinationReal %s" % destinationReal) + destinationReal = os.path.normpath(destinationReal) + if DEBUG: + print("normalized destinationReal %s" % destinationReal) + print("referenceFiles:", c["referenceFiles"]) + + if not os.path.isdir(destination): + if "git-directory" in c["referenceFiles"]: + # Sparse clone + os.makedirs(destination) + check_call_log(["git", "init"], stderr=subprocess.STDOUT, cwd=destinationReal) + check_call_log(["git", "remote", "add", "-f", "origin", giturl], stderr=subprocess.STDOUT, cwd=destinationReal) + check_call_log(["git", "config", "core.sparseCheckout", "true"], stderr=subprocess.STDOUT, cwd=destinationReal) + file = open(os.path.join(destinationReal,".git", "info", "sparse-checkout"), "a") + file.write(c["referenceFiles"]["git-directory"].strip()) + file.close() + else: + # Clone + check_call_log(["git", "clone", giturl, destination], stderr=subprocess.STDOUT) + + check_call_log(["git", "clean", "-fdx", "--exclude=*.hash"], stderr=subprocess.STDOUT, cwd=destinationReal) + check_call_log(["git", "fetch", "origin"], stderr=subprocess.STDOUT, cwd=destination) + # do not fail if the branch we were given doesn't exist + try: + check_call_log(["git", "reset", "--hard", refFilesGitTag], stderr=subprocess.STDOUT, cwd=destinationReal) + except subprocess.CalledProcessError as e: + print(e.output) + check_call_log(["git", "clean", "-fdx", "--exclude=*.hash"], stderr=subprocess.STDOUT, cwd=destinationReal) + if glob.glob(destinationReal + "/*.mat.xz"): + check_call_log(["find", ".", "-name", "*.mat.xz", "-exec", "xz", "--decompress", "--keep", "{}", ";"], stderr=subprocess.STDOUT, cwd=destinationReal) + try: + githash = check_output_log(["git", "rev-parse", "--verify", "HEAD"], stderr=subprocess.STDOUT, cwd=destinationReal, encoding='utf8') + except subprocess.CalledProcessError as e: + print(e.output) + + if "git-directory" in c["referenceFiles"]: + c["referenceFiles"] = os.path.join(destinationReal, c["referenceFiles"]['git-directory']) + print(c["referenceFiles"]) + else: + c["referenceFiles"] = destinationReal + + if giturl.startswith("https://github.com"): + c["referenceFilesURL"] = '%s (%s)' % (giturl, githash.strip(), giturl, githash.strip()) + else: + c["referenceFilesURL"] = "%s (%s)" % (giturl, githash.strip()) + preparedReferenceDirs[destination] = (c["referenceFiles"],c["referenceFilesURL"]) else: raise Exception("Unknown referenceFiles in config: %s" % (str(c["referenceFiles"]))) @@ -369,39 +559,10 @@ def testHelloWorld(cmd): # Create mos-files -conn = sqlite3.connect('sqlite3.db') -cursor = conn.cursor() - -user_version = cursor.execute("PRAGMA user_version").fetchone()[0] - -if user_version==0: - # BOOLEAN NOT NULL CHECK (verify IN (0,1) AND builds IN (0,1) AND simulates IN (0,1)) - # Table to lookup from a run (date, branch) to omcversion used - cursor.execute("CREATE TABLE if not exists [omcversion] (date integer NOT NULL, branch text NOT NULL, omcversion text NOT NULL)") - # Table to lookup from a run (date, branch) which library versions were used - cursor.execute("CREATE TABLE if not exists [libversion] (date integer NOT NULL, branch text NOT NULL, libname text NOT NULL, libversion text NOT NULL, confighash integer NOT NULL)") -elif user_version==1: - cursor.execute("ALTER TABLE [libversion] ADD COLUMN confighash integer NOT NULL DEFAULT(0)") -elif user_version==2: - tables = [tbl for (tbl,) in cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") if tbl not in ["libversion","omcversion"]] - for tbl in tables: - cursor.execute("ALTER TABLE [%s] ADD COLUMN parsing real NOT NULL DEFAULT(0.0)" % tbl) -elif user_version in [3]: - pass -else: - print("Unknown schema user_version=%d" % user_version) - sys.exit(1) - -cursor.execute('''CREATE TABLE if not exists [%s] - (date integer NOT NULL, libname text NOT NULL, model text NOT NULL, exectime real NOT NULL, - frontend real NOT NULL, backend real NOT NULL, simcode real NOT NULL, templates real NOT NULL, compile real NOT NULL, simulate real NOT NULL, - verify real NOT NULL, verifyfail integer NOT NULL, verifytotal integer NOT NULL, finalphase integer NOT NULL, parsing real NOT NULL)''' % branch) -cursor.execute('''DROP INDEX IF EXISTS [idx_%s_date]''' % branch) -cursor.execute('''DROP INDEX IF EXISTS idx_omcversion_date''') -cursor.execute('''DROP INDEX IF EXISTS idx_libversion_date''') - -# Set user_version to the current schema -cursor.execute("PRAGMA user_version=3") +db = resultsdb.connect(args.db) +cursor = db.cursor() +# One table per simulator, so this happens once per branch the run fills. +db.createTables(primaryBranch) def strToHashInt(s): return int(hashlib.sha1((s+"fixCorruptBuilds-2017-03-26").encode("utf-8")).hexdigest()[0:8],16) @@ -415,7 +576,7 @@ def findAllFiles(d): return res def getmd5(f): - hf = f+".hash" + hf = os.path.normpath(f+".hash") if not os.path.exists(hf) or (os.path.getmtime(f) > os.path.getmtime(hf)): with open(hf, "w") as fout: with open(f, "rb") as fin: @@ -436,23 +597,53 @@ def hashReferenceFiles(s): skipped_libs = {} tests=[] for (library,conf) in configs: + # Only when asked, so a normal run's confighash is unchanged + if args.nobuildmodel: + conf["noBuildModel"] = True + if args.coldhot: + conf["coldHot"] = True c=conf.copy() del(c["configFromFile"]) if "referenceFiles" in c: del(c["referenceFilesURL"]) + # Where the reference files happen to live on this machine must not go into + # the hash: their contents are hashed just below, and two machines sharing + # a database have to agree on what a configuration is. Each machine only + # ever had its own sqlite3 file before, so an absolute path was harmless. + c["referenceFiles"] = "referenceFiles" confighash = strToHashInt(str(c)+hashReferenceFiles(conf["referenceFiles"])) else: confighash = strToHashInt(str(c)+hashReferenceFiles("")) conf["confighash"] = confighash conf["omhome"] = omhome - conf["single_thread_cmd"] = single_thread + # if procOMC = 0 use max procs, use procOMC = 1 if not defined, else use the given value + if "procOMC" in conf: + if conf.get("procOMC") == 0: + omc_threads = '' + else: + omc_threads = '-n=%s' % conf.get("procOMC") + else: + omc_threads = '-n=1' + + # if procCompile = 0 use max procs, use procCompile = 1 if not defined, else use the given value + if "procCCompile" in conf: + if conf.get("procCCompile") == 0: + conf["procCCompile"] = n_cores + else: + conf["procCCompile"] = 1 + conf["omc_thread_cmd"] = omc_threads conf["haveCppRuntime"] = haveCppRuntime conf["ulimitMemory"] = conf.get("ulimitMemory") or ulimitMemory + if conf["simCodeTarget"]=="wasm-jit": + # The JIT reserves ~4 GB of address space per wasm memory on top of what omc + # itself maps, which does not fit in the default virtual memory limit + conf["ulimitMemory"] = max(conf["ulimitMemory"], 16*1024*1024) if conf.get("fmi"): conf["haveFMI"] = fmiOK_C conf["haveFMICpp"] = fmiOK_Cpp conf["fmisimulator"] = fmisimulator - conf["fmuType"] = conf.get("fmuType", "me") + conf["fmisimulators"] = ["%s=%s" % (n, c) for (n, c) in fmisimulators] + conf["fmuType"] = fmuType if (not canChangeOptLevel) and "optlevel" in conf: print("Deleting optlevel") del conf["optlevel"] @@ -460,7 +651,17 @@ def hashReferenceFiles(s): print("Failed to set Modelica grammar") sys.exit(1) omc.sendExpression('clear()') - for (lib,version) in [[library,conf["libraryVersion"]]] + conf.get("extraLibraries", []): + if "loadFileCommands" in conf: + for command in conf["loadFileCommands"]: + if not omc.sendExpression(command): + try: + print("Failed to run command %s: %s" % (command,omc.sendExpression('OpenModelica.Scripting.getErrorString()'))) + except: + print("Failed to run command %s OpenModelica.Scripting.getErrorString() failed..." % command) + librariesToLoad = [] + else: + librariesToLoad = [[library,conf["libraryVersion"]]] + conf.get("extraLibraries", []) + for (lib,version) in librariesToLoad: if conf["libraryVersionLatestInPackageManager"]: availableVersions = omc.sendExpression('getAvailablePackageVersions(%s,"%s")' % (lib,version)) if not availableVersions: @@ -471,23 +672,58 @@ def hashReferenceFiles(s): versions = "{" + ",".join(['"'+v+'"' for v in availableVersions]) + "}" else: versions = '{"%s"}' % version - if not omc.sendExpression('loadModel(%s,%s)' % (lib,versions)): + + exactMatch='' + if conf["libraryVersionExactMatch"]: + if conf["libraryVersionLatestInPackageManager"]: + raise Exception("Library %s has both libraryVersionLatestInPackageManager:true and libraryVersionExactMatch:true! Make up your mind." % libName) + exactMatch=', requireExactVersion=true' + + if not omc.sendExpression('loadModel(%s,%s%s)' % (lib,versions,exactMatch)): try: print("Failed to load library %s %s: %s" % (library,versions,omc.sendExpression('OpenModelica.Scripting.getErrorString()'))) except: print("Failed to load library %s %s. OpenModelica.Scripting.getErrorString() failed..." % (library,conf["libraryVersion"])) - conf["loadFiles"] = sorted(omc.sendExpression("{getSourceFile(cl) for cl in getClassNames()}")) - + # adrpo: do not sort the top level names as sometimes that loads a bad MSL version + # conf["loadFiles"] = sorted(omc.sendExpression("{getSourceFile(cl) for cl in getClassNames()}")) + conf["loadFiles"] = omc.sendExpression("{getSourceFile(cl) for cl in getClassNames()}") + if not (omc.sendExpression('setCommandLineOptions("-g=MetaModelica")') or omc.sendExpression('setCommandLineOptions("+g=Modelica")')): print("Failed to set MetaModelica grammar") sys.exit(1) + try: - conf["resourceLocation"]=omc.sendExpression('uriToFilename("modelica://%s/Resources")' % library)[0] + conf["libraryLocation"]=omc.sendExpression('uriToFilename("modelica://%s/")' % library) + conf["resourceLocation"]=omc.sendExpression('uriToFilename("modelica://%s/Resources")' % library) except: + conf["libraryLocation"]="" conf["resourceLocation"]="" + if "runOnceBeforeTesting" in conf: + for cmd in conf["runOnceBeforeTesting"]: + # replace the resource location in the command if present + cmd = [c.replace("$resourceLocation", conf["resourceLocation"]) for c in cmd] + cmd = [c.replace("$libraryLocation", conf["libraryLocation"]) for c in cmd] + check_call_log(cmd, stderr=subprocess.STDOUT) + + if "customCommands" in conf: + cmd = conf["customCommands"] + # replace the $libraryLocation in the customCommands if present + cmd = [c.replace("$libraryLocation", conf["libraryLocation"]) for c in cmd] + conf["customCommands"] = cmd + if "environmentSimulation" in conf: + cmd = conf["environmentSimulation"] + # replace the $libraryLocation in the environmentSimulation if present + cmd = [[el.replace("$libraryLocation", conf["libraryLocation"]) for el in c] for c in cmd] + conf["environmentSimulation"] = cmd + if "environmentTranslation" in conf: + cmd = conf["environmentTranslation"] + # replace the $libraryLocation in the environmentTraslation if present + cmd = [[el.replace("$libraryLocation", conf["libraryLocation"]) for el in c] for c in cmd] + conf["environmentTranslation"] = cmd + conf["libraryVersionRevision"]=omc.sendExpression('getVersion(%s)' % library) - librarySourceFile=omc.sendExpression('getSourceFile(%s)' % library) + librarySourceFile=os.path.normpath(omc.sendExpression('getSourceFile(%s)' % library)) lastChange=(librarySourceFile[:-3]+".last_change") if not librarySourceFile.endswith("package.mo") else (os.path.dirname(librarySourceFile)+".last_change") if os.path.exists(lastChange): conf["libraryLastChange"] = " %s (revision %s)" % (conf["libraryVersionRevision"],"\n".join(open(lastChange).readlines()).strip()) @@ -504,18 +740,30 @@ def hashReferenceFiles(s): if not conf["libraryVersionRevision"]: conf["libraryVersionRevision"] = conf["libraryLastChange"] if conf.get("fmi") and fmisimulatorversion: - conf["libraryVersionRevision"] = conf["libraryVersionRevision"] + " " + fmisimulatorversion - conf["libraryLastChange"] = conf["libraryLastChange"] + " " + fmisimulatorversion + conf["libraryVersionRevision"] = conf["libraryVersionRevision"] + " " + fmisimulatorversion.decode("ascii") + conf["libraryLastChange"] = conf["libraryLastChange"] + " " + fmisimulatorversion.decode("ascii") res=omc.sendExpression('{c for c guard isExperiment(c) and not regexBool(typeNameString(x), "^Modelica_Synchronous\\.WorkInProgress") in getClassNames(%s, recursive=true)}' % library) if conf.get("ignoreModelPrefix"): - prefix = conf["ignoreModelPrefix"] - res=list(filter(lambda x: not x.startswith(prefix), res)) + if isinstance(conf["ignoreModelPrefix"], list): + prefixes = conf["ignoreModelPrefix"] + for prefix in prefixes: + res=list(filter(lambda x: not x.startswith(prefix), res)) + else: # in this part it should be only a string + prefix = conf["ignoreModelPrefix"] + res=list(filter(lambda x: not x.startswith(prefix), res)) libName=shared.libname(library, conf) - v = cursor.execute("""SELECT date,libversion,libname,branch,omcversion FROM [libversion] NATURAL JOIN [omcversion] - WHERE libversion=? AND libname=? AND branch=? AND omcversion=? AND confighash=? ORDER BY date DESC LIMIT 1""", (conf["libraryLastChange"],libName,branch,omc_version,confighash)).fetchone() + v = cursor.execute("""SELECT date,libversion,libname,branch,omcversion FROM libversion NATURAL JOIN omcversion + WHERE libversion=? AND libname=? AND branch=? AND omcversion=? AND confighash=? ORDER BY date DESC LIMIT 1""", (conf["libraryLastChange"],libName,primaryBranch,omc_version,confighash)).fetchone() if libName in stats_by_libname or libName in skipped_libs: raise Exception("Duplicate libName found: %s" % libName) - if v is None: + if v is None or execAllTests: + # On the shared database another machine may already be running this exact + # job; claiming it is what keeps the two from testing the same thing. + if not db.claim(primaryBranch, libName, conf["libraryLastChange"], omc_version, confighash): + (host, since) = db.claimedBy(primaryBranch, libName, conf["libraryLastChange"], omc_version, confighash) + print("Skipping %s as %s has been testing it since %s" % (libName, host, since)) + skipped_libs[libName] = None + continue stats_by_libname[libName] = {"conf":conf, "stats":[]} tests = tests + [(r,library,libName,libName+"_"+r,conf) for r in res] print("Running library %s (%d tests)" % (libName, len(res))) @@ -545,7 +793,8 @@ def hashReferenceFiles(s): (u"#customCommands#", conf["customCommands"]), (u"#modelVersion#", conf["libraryVersion"]), (u"#ulimitOmc#", str(conf["ulimitOmc"])), - (u"#default_tolerance#", str(conf["default_tolerance"])), + (u"#defaultTolerance#", str(conf["defaultTolerance"])), + (u"#defaultNumberOfIntervals#", str(conf["defaultNumberOfIntervals"])), (u"#reference_reltol#", str(conf["reference_reltol"])), (u"#reference_reltolDiffMinMax#", str(conf["reference_reltolDiffMinMax"])), (u"#reference_rangeDelta#", str(conf["reference_rangeDelta"])), @@ -556,11 +805,19 @@ def hashReferenceFiles(s): ) with open(name + ".conf.json", 'w') as fp: newconf = dict(conf.items()) + if "referenceFiles" in newconf: + newconf["referenceFiles"] = newconf["referenceFiles"].replace("\\","/") + if "referenceFilesURL" in newconf: + newconf["referenceFilesURL"] = newconf["referenceFilesURL"].replace("\\","/") + if "libraryLocation" in newconf: + newconf["libraryLocation"] = newconf["libraryLocation"].replace("\\","/") + if "libraryLastChange" in newconf: + newconf["libraryLastChange"] = newconf["libraryLastChange"].replace("\\","/") newconf["library"] = library newconf["modelName"] = modelName newconf["fileName"] = name try: - newconf["referenceFile"] = getReferenceFileName(newconf) + newconf["referenceFile"] = getReferenceFileName(newconf).replace("\\","/") except Exception as e: # Find all such errors print(e) @@ -572,28 +829,38 @@ def hashReferenceFiles(s): print("Created .conf.json files") sys.stdout.flush() -def runScript(c, timeout, memoryLimit): - j = "files/%s.stat.json" % c +def runScript(c, timeout, memoryLimit, runverbose): + j = os.path.normpath("files/%s.stat.json" % c) try: os.remove(j) except: pass start=monotonic() - # runCommand("%s %s %s.mos" % (omc_exe, single_thread, c), prefix=c, timeout=timeout) - if 0 != runCommand("ulimit -v %d; ./testmodel.py --libraries=%s %s --ompython_omhome=%s %s %s.conf.json > files/%s.cmdout 2>&1" % (memoryLimit, librariespath, ("--docker %s --dockerExtraArgs '%s'" % (docker, " ".join(dockerExtraArgs))) if docker else "", ompython_omhome, "--corba" if corbaStyle else "", c, c), prefix=c, timeout=timeout): + # runCommand("%s %s %s.mos" % (omc_exe, omc_threads, c), prefix=c, timeout=timeout) + if runverbose: + print("Starting test: %s" % c) + sys.stdout.flush() + + if isWin: + res_cmd = runCommand("%s testmodel.py --win %s --msysEnvironment=%s --libraries=\"%s\" %s --ompython_omhome=%s %s.conf.json > files/%s.cmdout 2>&1" % (pythonExecutablePopenWin, '--addmsl' if addmsl else "", msysEnvironment, librariespath, ("--docker %s --dockerExtraArgs '%s'" % (docker, " ".join(dockerExtraArgs))) if docker else "", ompython_omhome, c, c), prefix=c, timeout=timeout) + else: + res_cmd = runCommand("ulimit -v %d; ./testmodel.py %s --libraries=%s %s --ompython_omhome=%s %s.conf.json > files/%s.cmdout 2>&1" % (memoryLimit, '--addmsl' if addmsl else "", librariespath, ("--docker %s --dockerExtraArgs '%s'" % (docker, " ".join(dockerExtraArgs))) if docker else "", ompython_omhome, c, c), prefix=c, timeout=timeout) + + if res_cmd != 0: print("files/%s.err" % c) - with open("files/%s.err" % c, "a+") as errfile: + with open(os.path.normpath("files/%s.err" % c), "a+") as errfile: errfile.write("Failed to read output from testmodel.py, exit status != 0:\n") try: - with open("files/%s.cmdout" % c) as cmdout: + with open(os.path.normpath("files/%s.cmdout" % c)) as cmdout: errfile.write(cmdout.read()) except IOError: pass except OSError: pass + if clean: try: - os.unlink("files/%s.cmdout" % c) + os.unlink(os.path.normpath("files/%s.cmdout" % c)) except OSError: pass @@ -605,12 +872,15 @@ def runScript(c, timeout, memoryLimit): data = {"phase":0} data["exectime"] = execTime json.dump(data, open(j,"w")) + if runverbose: + print("[%s %ds] " % (c, execTime)) + sys.stdout.flush() def expectedExec(c): (model,lib,libName,name,data) = c if "expectedExec" in data: return data["expectedExec"] - cursor.execute("SELECT exectime FROM [%s] WHERE libname = ? AND model = ? ORDER BY date DESC LIMIT 1" % branch, (libName,model)) + cursor.execute("SELECT exectime FROM %s WHERE libname = ? AND model = ? ORDER BY date DESC LIMIT 1" % db.quote(primaryBranch), (libName,model)) v = cursor.fetchone() data["expectedExec"] = (v or (0.0,))[0] return data["expectedExec"] @@ -635,19 +905,28 @@ def expectedExec(c): print("Created files directory") sys.stdout.flush() -if len(tests)==0: +numberOfTests = len(tests) + +if numberOfTests==0: print("Everything already up to date. Not executing any tests.") sys.exit(0) -print("Starting execution of %d tests. Estimated execution time %s (wrong if there are new or few tests)." % (len(tests), friendlyStr(sum(expectedExec(c) for c in tests)/(1.0*n_jobs)))) +print("Starting execution of %d tests. Estimated execution time %s (wrong if there are new or few tests).\n" % (numberOfTests, friendlyStr(sum(expectedExec(c) for c in tests)/(1.0*n_jobs)))) sys.stdout.flush() cmd_res=[0] start=monotonic() start_as_time=time.localtime() testRunStartTimeAsEpoch = int(time.time()) # Need translateModel + make + exe... -cmd_res=Parallel(n_jobs=n_jobs)(delayed(runScript)(name, 2*data["ulimitOmc"]+data["ulimitExe"]+25, data["ulimitMemory"]) for (model,lib,libName,name,data) in tests) +if n_jobs == 1: + verbose = 10 +else: + verbose = 5 +if customTimeout > 0.0: + cmd_res=Parallel(n_jobs=n_jobs, verbose=verbose)(delayed(runScript)(name, customTimeout, data["ulimitMemory"], runverbose) for (model,lib,libName,name,data) in tests) +else: + cmd_res=Parallel(n_jobs=n_jobs, verbose=verbose)(delayed(runScript)(name, 2*data["ulimitOmc"]+data["ulimitExe"]+25, data["ulimitMemory"], runverbose) for (model,lib,libName,name,data) in tests) stop=monotonic() print("Execution time: %s" % friendlyStr(stop-start)) assert(stop-start >= 0.0) @@ -665,10 +944,40 @@ def loadJsonOrEmptySet(f): #for k in sorted(stats.keys(), key=lambda c: stats[c][3]["exectime"], reverse=True): # print("%s: exectime %.2f" % (k, stats[k][3]["exectime"])) -for key in stats.keys(): - (name,model,libname,data)=stats[key] - stats_by_libname[libname]["stats"].append(stats[key]) - values = (testRunStartTimeAsEpoch, +if args.coldhot: + # Only "sim" (the hot run) is stored; the cold one is reported here + print("Cold vs hot simulation time:") + for key in sorted(stats.keys(), key=lambda k: stats[k][1]): + (name,model,libname,data)=stats[key] + if data.get("simcold") is not None: + print(" %-70s cold %8.4f hot %8.4f" % (model, data["simcold"], data.get("sim") or 0.0)) + sys.stdout.flush() + +BUILD_PHASE = 5 +"""The last phase a model shares with every simulator of its FMU.""" + +def phaseWithoutSimulator(data): + """How far a model got for a simulator that never ran it. + + Everything up to the build is shared, so such a model reports the phase the + build reached and never the phase another simulator went on to reach with the + same FMU. Reporting that one would claim a simulation, and a verification, + that this simulator never performed. + """ + return min(data.get("phase") or 0, BUILD_PHASE) + +def resultValues(model, libname, data, simulator=None): + """One row of a branch table. + + Everything up to the build is what the model cost whatever simulates it, so + the simulators of one FMU share it and differ only in the simulation, the + verification and how far they got. + """ + simulated = data if simulator is None else (data.get("simulators") or {}).get(simulator) + if simulated is None: + simulated = {"phase": phaseWithoutSimulator(data)} + diff = simulated.get("diff") or {} + return (testRunStartTimeAsEpoch, libname, model, data.get("exectime") or 0.0, @@ -677,19 +986,46 @@ def loadJsonOrEmptySet(f): data.get("simcode") or 0.0, data.get("templates") or 0.0, data.get("build") or 0.0, - data.get("sim") or 0.0, - (data.get("diff") or {}).get("time") or 0.0, - len((data.get("diff") or {}).get("vars") or []), - (data.get("diff") or {}).get("numCompared") or 0, - data.get("phase") or 0, + simulated.get("sim") or 0.0, + diff.get("time") or 0.0, + len(diff.get("vars") or []), + diff.get("numCompared") or 0, + simulated.get("phase") or 0, data.get("parsing") or 0.0 ) - # print values - cursor.execute("INSERT INTO [%s] VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" % branch, values) -for libname in stats_by_libname.keys(): - confighash = stats_by_libname[libname]["conf"]["confighash"] - cursor.execute("INSERT INTO [libversion] VALUES (?,?,?,?,?)", (testRunStartTimeAsEpoch, branch, libname, stats_by_libname[libname]["conf"]["libraryLastChange"], confighash)) -cursor.execute("INSERT INTO [omcversion] VALUES (?,?,?)", (testRunStartTimeAsEpoch, branch, omc_version)) + +def cpu_name(): + if isWin: + return processor() + else: + for line in open("/proc/cpuinfo").readlines(): + if "model name" in line.strip(): + return (re.sub( ".*model name.*:", "", line, count=1)).strip() + +if isWin: + lsb_release = "" +else: + try: + lsb_release = check_output_log(commands + ["cat","/etc/lsb-release"]).decode().strip() + lsb_release = dict(a.split("=") for a in lsb_release.split("\n"))["DISTRIB_DESCRIPTION"].strip('"') + except: + lsb_release = "" + +hostname = socket.gethostname() +sysInfo = "%s: %s, %d GB RAM, %s%s" % (hostname, cpu_name(), int(math.ceil(psutil.virtual_memory().total / (1024.0**3))), ("Docker " + docker + " ") if docker else "", lsb_release) + +for (resultBranch, simulator) in resultBranches: + db.createTables(resultBranch) + for key in stats.keys(): + (name,model,libname,data)=stats[key] + if simulator is None: + stats_by_libname[libname]["stats"].append(stats[key]) + cursor.execute("INSERT INTO %s VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)%s" % (db.quote(resultBranch), db.insertIgnore()), + resultValues(model, libname, data, simulator)) + for libname in stats_by_libname.keys(): + confighash = stats_by_libname[libname]["conf"]["confighash"] + cursor.execute("INSERT INTO libversion VALUES (?,?,?,?,?,?,?)%s" % db.insertIgnore(), (testRunStartTimeAsEpoch, resultBranch, libname, stats_by_libname[libname]["conf"]["libraryLastChange"], confighash, hostname, sysInfo)) + cursor.execute("INSERT INTO omcversion VALUES (?,?,?)%s" % db.insertIgnore(), (testRunStartTimeAsEpoch, resultBranch, omc_version)) """ # Not really a good thing to do; was just done to make generation of the report simpler for libname in skipped_libs.keys(): @@ -711,138 +1047,320 @@ def checkPhase(phase, n): return "#FFCC66" def is_non_zero_file(fpath): - return os.path.isfile(fpath) and os.path.getsize(fpath) > 0 - -def cpu_name(): - for line in open("/proc/cpuinfo").readlines(): - if "model name" in line.strip(): - return (re.sub( ".*model name.*:", "", line,1)).strip() - -try: - lsb_release = subprocess.check_output(commands + ["cat","/etc/lsb-release"]).decode().strip() - lsb_release = dict(a.split("=") for a in lsb_release.split("\n"))["DISTRIB_DESCRIPTION"].strip('"') -except: - lsb_release = "" - -sysInfo = "%s, %d GB RAM, %s%s" % (cpu_name(), int(math.ceil(psutil.virtual_memory().total / (1024.0**3))), ("Docker " + docker + " ") if docker else "", lsb_release) - -htmltpl=open("library.html.tpl").read() -for libname in stats_by_libname.keys(): - if libname in skipped_libs: - continue - s = None # Make sure I don't use this - filesList = open(libname + ".files", "w") - filesList.write("/\n") - filesList.write("/%s.html\n" % libname) - filesList.write("/files/\n") - conf = stats_by_libname[libname]["conf"] - stats = stats_by_libname[libname]["stats"] - for s in stats: - filename_prefix = "files/%s_%s" % (s[2],s[1]) - filesList.write("/%s*diff*csv\n" % filename_prefix) - filesList.write("/%s*diff*html\n" % filename_prefix) - if is_non_zero_file(filename_prefix+".sim"): - filesList.write("/%s.sim\n" % filename_prefix) - if is_non_zero_file(filename_prefix+".err"): - filesList.write("/%s.err\n" % filename_prefix) - variables = (s[3].get("diff") or {}).get("vars") or [] - if len(variables)>0: - filesList.write("/%s.diff.html\n" % filename_prefix) - for v in variables: - filesList.write("/%s.diff.%s.csv\n" % (filename_prefix, v)) - filesList.write("/%s.diff.%s.html\n" % (filename_prefix, v)) - filesList.close() - testsHTML = "\n".join(['\n' % - (lambda filename_prefix, diff: - ( - ('%s' % (filename_prefix + ".err", html.escape(s[1]))) if is_non_zero_file(filename_prefix + ".err") else html.escape(s[1]), - (' (sim)' % (filename_prefix + ".sim")) if is_non_zero_file(filename_prefix + ".sim") else "", - checkPhase(s[3]["phase"], 7) if s[3]["phase"]>=6 else "#FFFFFF", - ("%s (%d verified)" % (timeSeconds(diff.get("time")), diff.get("numCompared"))) if s[3]["phase"]>=7 else (" " if diff is None else - ('%s (%d/%d failed)' % (timeSeconds(diff.get("time")), filename_prefix, len(diff.get("vars")), diff.get("numCompared")))), - checkPhase(s[3]["phase"], 6), - timeSeconds(s[3].get("sim") or 0), - checkPhase(s[3]["phase"], 5), - timeSeconds(sum(s[3].get(x) or 0.0 for x in ["frontend","backend","simcode","templates","build"])), - timeSeconds(s[3].get("parsing") or 0), - checkPhase(s[3]["phase"], 1), - timeSeconds(s[3].get("frontend") or 0), - checkPhase(s[3]["phase"], 2), - timeSeconds(s[3].get("backend") or 0), - checkPhase(s[3]["phase"], 3), - timeSeconds(s[3].get("simcode") or 0), - checkPhase(s[3]["phase"], 4), - timeSeconds(s[3].get("templates") or 0), - checkPhase(s[3]["phase"], 5), - timeSeconds(s[3].get("build") or 0) - ))(filename_prefix="files/%s_%s" % (s[2], s[1]), diff=s[3].get("diff")) - for s in natsorted(stats, key=lambda s: s[1])]) - numSucceeded = [len(stats)] + [sum(1 if s[3]["phase"]>=i else 0 for s in stats) for i in range(1,8)] - replacements = ( - (u"#sysInfo#", html.escape(sysInfo)), - (u"#omcVersion#", html.escape(omc_version)), - (u"#fmi#", ("

"+html.escape("FMI version: %s" % conf.get("fmi"))+"

") if conf.get("fmi") else ""), - (u"#optlevel#", html.escape(conf.get("optlevel")) if (canChangeOptLevel and conf.get("optlevel")) else "Tool default"), - (u"#timeStart#", html.escape(time.strftime('%Y-%m-%d %H:%M:%S', start_as_time))), - (u"#fileName#", html.escape(libname)), - (u"#customCommands#", html.escape("\n".join(conf["customCommands"]))), - (u"#libraryVersionRevision#", html.escape(conf["libraryVersionRevision"])), - (u"#metadata#", html.escape(conf["metadata"])), - (u"#ulimitOmc#", html.escape(str(conf["ulimitOmc"]))), - (u"#ulimitExe#", html.escape(str(conf["ulimitExe"]))), - (u"#default_tolerance#", html.escape(str(conf["default_tolerance"]))), - (u"#simFlags#", html.escape(conf.get("simFlags") or "")), - (u"#referenceFiles#", ('

Reference Files: %s

' % conf["referenceFilesURL"].replace(os.path.dirname(os.path.realpath(__file__)),"")) if ((conf.get("referenceFilesURL") or "") != "") else ""), - (u"#referenceTool#", ('

Verified using: %s (diffSimulationResults)

' % html.escape(ompython_omc_version)) if ((conf.get("referenceFiles") or "") != "") else ""), - (u"#Total#", html.escape(str(numSucceeded[0]))), - (u"#FrontendColor#", checkNumSucceeded(numSucceeded, 1)), - (u"#BackendColor#", checkNumSucceeded(numSucceeded, 2)), - (u"#SimCodeColor#", checkNumSucceeded(numSucceeded, 3)), - (u"#TemplatesColor#", checkNumSucceeded(numSucceeded, 4)), - (u"#CompilationColor#", checkNumSucceeded(numSucceeded, 5)), - (u"#SimulationColor#", checkNumSucceeded(numSucceeded, 6)), - (u"#VerificationColor#", checkNumSucceeded(numSucceeded, 7)), - (u"#Frontend#", html.escape(str(numSucceeded[1]))), - (u"#Backend#", html.escape(str(numSucceeded[2]))), - (u"#SimCode#", html.escape(str(numSucceeded[3]))), - (u"#Templates#", html.escape(str(numSucceeded[4]))), - (u"#Compilation#", html.escape(str(numSucceeded[5]))), - (u"#Simulation#", html.escape(str(numSucceeded[6]))), - (u"#Verification#", html.escape(str(numSucceeded[7]))), - (u"#totalTime#", html.escape(str(datetime.timedelta(seconds=int(sum(s[3].get("exectime") or 0.0 for s in stats)))))), - (u"#config#", html.escape(json.dumps(conf["configFromFile"], indent=1, sort_keys=True))), - (u"#testsHTML#", testsHTML) - ) - open("%s.html" % libname, "w").write(multiple_replace(htmltpl, *replacements)) - if result_location != "": - result_location_libname = "%s/%s" % (result_location, libname) + return os.path.isfile(os.path.normpath(fpath)) and os.path.getsize(os.path.normpath(fpath)) > 0 + +# create target dir to move results without sync operations (win or when --noSync is used) +def stageRootFor(simulator, suffix): + """The directory a branch is published from. + + The first simulator publishes the workspace itself, as a job always has. The + others write _.sim there so as not to overwrite it, and are + published from a directory of their own where the same files appear under the + plain .sim a branch directory has always held. + """ + if not suffix: + return "." + root = "publish_%s" % simulator + for d in (root, os.path.join(root, "files")): try: - os.mkdir("emptydir") - except: + os.mkdir(d) + except OSError: pass - subprocess.check_output(["rsync", "-aR", "emptydir/", result_location]) - subprocess.check_output(["rsync", "-aR", "emptydir/", result_location_libname]) - subprocess.check_output(["rsync", "-aR", "emptydir/", result_location_libname+"/files"]) + return root + +def stagePublished(stageRoot, workspacePrefix, publishedPrefix, suffix): + """Link one model's results into the directory its branch is published from.""" + if not suffix: + return + for f in glob.glob(glob.escape(workspacePrefix) + "*"): + published = os.path.join(stageRoot, publishedPrefix + f[len(workspacePrefix):]) try: - subprocess.check_output(["rsync", "-aR", "--delete-excluded", "--include-from=%s.files" % libname, "--exclude=*", "./", result_location_libname]) - except: - subprocess.check_output(["rsync", "-aR", "emptydir/", result_location]) - subprocess.check_output(["rsync", "-aR", "emptydir/", result_location_libname]) - subprocess.check_output(["rsync", "-aR", "emptydir/", result_location_libname+"/files"]) - subprocess.check_output(["rsync", "-aR", "--delete-excluded", "--include-from=%s.files" % libname, "--exclude=*", "./", result_location_libname]) - if (conf.get("referenceFiles") or "") != "": - subprocess.check_output(["rsync", "-a", dygraphs, result_location_libname+"/files"]) + if os.path.exists(published): + os.unlink(published) + os.link(f, published) + except OSError: + shutil.copy2(f, published) + +def stageShared(stageRoot, prefix, suffix): + """The .err of the build belongs to every simulator of the model.""" + if not suffix: + return + for f in glob.glob(glob.escape(prefix) + ".err"): + published = os.path.join(stageRoot, f) + try: + if os.path.exists(published): + os.unlink(published) + os.link(f, published) + except OSError: + shutil.copy2(f, published) + +def dataForSimulator(data, simulator): + """The results of one model as one simulator saw them. + + Everything up to the build is shared, so only the simulation, the comparison + against the reference file and the phase come from the simulator itself. + """ + if simulator is None: + return data + simulated = (data.get("simulators") or {}).get(simulator) + merged = dict(data) + merged["sim"] = (simulated or {}).get("sim") + merged["diff"] = (simulated or {}).get("diff") + # A model that never got as far as being simulated stopped in the build, + # which every simulator of it shares. + merged["phase"] = simulated.get("phase") if simulated is not None else phaseWithoutSimulator(data) + return merged + +def artifactSuffix(simulator): + """What tells the files of one simulator from those of another.""" + return "_%s" % simulator if simulator and len(fmisimulators) > 1 else "" + +# Every simulator publishes its own results - .sim and diff files included - to +# the directory of its own branch; the .err of the build is shared, so each of +# them gets a copy of it. +for (resultBranch, simulator) in resultBranches: + result_location = outputFor(resultBranch) + suffix = artifactSuffix(simulator) + if result_location != "" and (isWin or noSync): + # Unlike the rsync path, this one is given the directory the branches live + # in and appends the branch itself. + resRootPath = os.path.join(jobOutput, resultBranch) + if os.path.exists(resRootPath): + rmtree(resRootPath) + os.makedirs(resRootPath) + + htmltpl=open("library.html.tpl").read() + for libname in stats_by_libname.keys(): + if libname in skipped_libs: + continue + s = None # Make sure I don't use this + stageRoot = stageRootFor(simulator, suffix) + filesList = open(os.path.join(stageRoot, libname + ".files"), "w") + filesList.write("/\n") + filesList.write("/%s.html\n" % libname) + filesList.write("/files/\n") + conf = stats_by_libname[libname]["conf"] + stats = [(n, m, l, dataForSimulator(d, simulator)) for (n, m, l, d) in stats_by_libname[libname]["stats"]] + for s in stats: + # What the simulator wrote in the workspace, and what it is published as: + # the branch of a simulator holds its results under the plain name. + workspace_prefix = "files/%s_%s%s" % (s[2],s[1],suffix) + filename_prefix = "files/%s_%s" % (s[2],s[1]) + stagePublished(stageRoot, workspace_prefix, filename_prefix, suffix) + stageShared(stageRoot, "files/%s_%s" % (s[2],s[1]), suffix) + filesList.write("/%s*diff*csv\n" % filename_prefix) + filesList.write("/%s*diff*html\n" % filename_prefix) + if is_non_zero_file(workspace_prefix+".sim"): + filesList.write("/%s.sim\n" % filename_prefix) + errPrefix = "files/%s_%s" % (s[2],s[1]) + if is_non_zero_file(errPrefix+".err"): + filesList.write("/%s.err\n" % errPrefix) + variables = (s[3].get("diff") or {}).get("vars") or [] + if len(variables)>0: + filesList.write("/%s.diff.html\n" % filename_prefix) + for v in variables: + filesList.write("/%s.diff.%s.csv\n" % (filename_prefix, v)) + filesList.write("/%s.diff.%s.html\n" % (filename_prefix, v)) + filesList.close() + testsHTML = "\n".join(['\n' % + (lambda filename_prefix, errPrefix, diff: + ( + ('%s' % (errPrefix + ".err", html.escape(s[1]))) if is_non_zero_file(errPrefix + ".err") else html.escape(s[1]), + (' (sim)' % (filename_prefix + ".sim")) if is_non_zero_file(filename_prefix + suffix + ".sim") else "", + checkPhase(s[3]["phase"], 7) if s[3]["phase"]>=6 else "#FFFFFF", + # Nothing was compared, whatever phase the model reports. + " " if diff is None else + (("%s (%d verified)" % (timeSeconds(diff.get("time")), diff.get("numCompared"))) if s[3]["phase"]>=7 else + ('%s (%d/%d failed)' % (timeSeconds(diff.get("time")), filename_prefix, len(diff.get("vars")), diff.get("numCompared")))), + checkPhase(s[3]["phase"], 6), + timeSeconds(s[3].get("sim") or 0), + checkPhase(s[3]["phase"], 5), + timeSeconds(sum(s[3].get(x) or 0.0 for x in ["frontend","backend","simcode","templates","build"])), + timeSeconds(s[3].get("parsing") or 0), + checkPhase(s[3]["phase"], 1), + timeSeconds(s[3].get("frontend") or 0), + checkPhase(s[3]["phase"], 2), + timeSeconds(s[3].get("backend") or 0), + checkPhase(s[3]["phase"], 3), + timeSeconds(s[3].get("simcode") or 0), + checkPhase(s[3]["phase"], 4), + timeSeconds(s[3].get("templates") or 0), + checkPhase(s[3]["phase"], 5), + timeSeconds(s[3].get("build") or 0), + timeSeconds(s[3].get("exectime") or 0) + ))(filename_prefix="files/%s_%s" % (s[2], s[1]), errPrefix="files/%s_%s" % (s[2], s[1]), diff=s[3].get("diff")) + for s in natsorted(stats, key=lambda s: s[1])]) + numSucceeded = [len(stats)] + [sum(1 if s[3]["phase"]>=i else 0 for s in stats) for i in range(1,8)] + + try: + githuburltesting = "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/OpenModelica/OpenModelicaLibraryTesting/commit/" + gitloglibrarytesting = check_output_log(["git", "log", '--pretty=
%s%s%s%s%s%s%s%s%s%s
%s%s%s%s%d
%s%s%s%s%d
%s%s%s%s%s%s%s%s%s%s%s
%s%s%s%s%s%s%s%s%s%s%s%s
CommitDateAuthorSummary
%%h%%ai%%an%%s
' % (githuburltesting), "-1"], cwd="./").decode("utf-8") + except subprocess.CalledProcessError as e: + print(str(e)) + gitloglibrarytesting = "
could not get the git log for OpenModelicaLibraryTesting
" + + # adrpo: attempt to get the revision of the reference files if possible + if conf.get("referenceFiles"): + c = conf.get("referenceFiles") + if DEBUG: + print("referenceFiles git ... attempting to retrieve info from directory: %s" % c) + sys.stdout.flush() + gitReferenceFiles = c + # see if we have a commit file + f = os.path.join(c, "commit") + if os.path.exists(f): + with open(f) as fin: + gitReferenceFilesVersion = fin.read() + print("referenceFiles git ... read from file %s" % f) + sys.stdout.flush() + else: + try: + if isinstance(c, (str, bytes)): + if DEBUG: + print("referenceFiles git ... see if directory has an evironment variable") + sys.stdout.flush() + m = re.search("^[$][A-Z_]+", c) + if m: + k = m.group(0)[1:] + if k not in os.environ: + if DEBUG: + print("referenceFiles git ... environment variable used in the directory cannot be found in the environment: %s" % k) + sys.stdout.flush() + raise Exception("Environment variable %s not defined, but used in JSON config for reference files" % k) + gitReferenceFiles = c.replace(m.group(0), os.environ[k]) + if DEBUG: + print("referenceFiles git ... directory after replacing the environment variable: %s" % gitReferenceFiles) + sys.stdout.flush() + sys.stdout.flush() + try: + gitReferenceFilesURL = check_output_log(["git", "config", "--get", "remote.origin.url"], cwd=gitReferenceFiles).decode("utf-8") + except subprocess.CalledProcessError as e: + print(e) + gitReferenceFilesURL = gitReferenceFiles + gitReferenceFilesVersion = check_output_log(["git", "log", '--pretty=
CommitDateAuthorSummary
%%h%%ai%%an%%s
' % (gitReferenceFilesURL), "-1"], cwd=gitReferenceFiles).decode("utf-8") + print("referenceFiles git ... got version information: %s" % gitReferenceFilesVersion) + sys.stdout.flush() + except subprocess.CalledProcessError as e: + print("referenceFiles git ... something went wrong with getting the git info for directory: %s" % c) + print(str(e)) + sys.stdout.flush() + gitReferenceFilesVersion = "" + else: + gitReferenceFilesVersion = "" + + replacements = ( + (u"#sysInfo#", html.escape(sysInfo)), + (u"#omcVersion#", html.escape(omc_version)), + (u"#fmiToolVersion#", ("

"+html.escape("FMI tool: %s" % fmisimulatorversion)+"

") if fmisimulatorversion else ""), + (u"#fmi#", ("

"+html.escape("FMI version: %s" % conf.get("fmi"))+"

") if conf.get("fmi") else ""), + (u"#optlevel#", html.escape(conf.get("optlevel")) if (canChangeOptLevel and conf.get("optlevel")) else "Tool default"), + (u"#timeStart#", html.escape(time.strftime('%Y-%m-%d %H:%M:%S', start_as_time))), + (u"#fileName#", html.escape(libname)), + (u"#customCommands#", html.escape("\n".join(conf["customCommands"]))), + (u"#libraryVersionRevision#", html.escape(conf["libraryVersionRevision"])), + (u"#OpenModelicaLibraryTesting#", gitloglibrarytesting), + (u"#metadata#", html.escape(conf["metadata"])), + (u"#ulimitOmc#", html.escape(str(conf["ulimitOmc"]))), + (u"#ulimitExe#", html.escape(str(conf["ulimitExe"]))), + (u"#defaultTolerance#", html.escape(str(conf["defaultTolerance"]))), + (u"#defaultNumberOfIntervals#", html.escape(str(conf["defaultNumberOfIntervals"]))), + (u"#simFlags#", html.escape(conf.get("simFlags") or "")), + (u"#referenceFiles#", ('

Reference Files: %s

%s' % ((conf["referenceFilesURL"].replace(os.path.dirname(os.path.realpath(__file__)),"")), gitReferenceFilesVersion)) if ((conf.get("referenceFilesURL") or "") != "") else ""), + (u"#referenceTool#", ('

Verified using: %s (diffSimulationResults)

' % html.escape(ompython_omc_version)) if ((conf.get("referenceFiles") or "") != "") else ""), + (u"#Total#", html.escape(str(numSucceeded[0]))), + (u"#FrontendColor#", checkNumSucceeded(numSucceeded, 1)), + (u"#BackendColor#", checkNumSucceeded(numSucceeded, 2)), + (u"#SimCodeColor#", checkNumSucceeded(numSucceeded, 3)), + (u"#TemplatesColor#", checkNumSucceeded(numSucceeded, 4)), + (u"#CompilationColor#", checkNumSucceeded(numSucceeded, 5)), + (u"#SimulationColor#", checkNumSucceeded(numSucceeded, 6)), + (u"#VerificationColor#", checkNumSucceeded(numSucceeded, 7)), + (u"#Frontend#", html.escape(str(numSucceeded[1]))), + (u"#Backend#", html.escape(str(numSucceeded[2]))), + (u"#SimCode#", html.escape(str(numSucceeded[3]))), + (u"#Templates#", html.escape(str(numSucceeded[4]))), + (u"#Compilation#", html.escape(str(numSucceeded[5]))), + (u"#Simulation#", html.escape(str(numSucceeded[6]))), + (u"#Verification#", html.escape(str(numSucceeded[7]))), + (u"#totalTime#", html.escape(str(datetime.timedelta(seconds=int(sum(s[3].get("exectime") or 0.0 for s in stats)))))), + (u"#config#", html.escape(json.dumps(conf["configFromFile"], indent=1, sort_keys=True))), + (u"#testsHTML#", testsHTML) + ) + open(os.path.join(stageRoot, "%s.html" % libname), "w").write(multiple_replace(htmltpl, *replacements)) + + # move results by sync operations (not available under win) + if result_location != "" and not isWin and not noSync: + result_location_libname = "%s/%s" % (result_location, libname) + try: + os.mkdir(os.path.join(stageRoot, "emptydir")) + except: + pass + check_output_log(["rsync", "-aR", "emptydir/", result_location], cwd=stageRoot) + check_output_log(["rsync", "-aR", "emptydir/", result_location_libname], cwd=stageRoot) + check_output_log(["rsync", "-aR", "emptydir/", result_location_libname+"/files"], cwd=stageRoot) + try: + check_output_log(["rsync", "-aR", "--delete-excluded", "--include-from=%s.files" % libname, "--exclude=*", "./", result_location_libname], cwd=stageRoot) + except: + check_output_log(["rsync", "-aR", "emptydir/", result_location], cwd=stageRoot) + check_output_log(["rsync", "-aR", "emptydir/", result_location_libname], cwd=stageRoot) + check_output_log(["rsync", "-aR", "emptydir/", result_location_libname+"/files"], cwd=stageRoot) + check_output_log(["rsync", "-aR", "--delete-excluded", "--include-from=%s.files" % libname, "--exclude=*", "./", result_location_libname], cwd=stageRoot) + if (conf.get("referenceFiles") or "") != "" and dygraphs: + check_output_log(["rsync", "-a", dygraphs, result_location_libname+"/files"]) + else: + print("No Sync: result_location [%s] != "" and not isWin [%s] and not noSync [%s] : library: %s" % (result_location, isWin, noSync, libname)) + + # move results without sync operations (win or when --noSync is used) + if result_location != "" and (isWin or noSync): + print("--> copy res file of library: " + libname) + libPath = os.path.join(resRootPath, libname) + if not os.path.exists(libPath): + os.makedirs(libPath) + libFilesPath = os.path.join(libPath, 'files') + if os.path.exists(libFilesPath): + rmtree(libFilesPath) + os.makedirs(libFilesPath) + try: + if clean: + shutil.move("./" + libname + ".html", libPath) + else: + shutil.copy2("./" + libname + ".html", libPath) + except: + print("-- problem durin copy/move of html file of lib: " + libname) + + for file in glob.glob("./files/" + libname + '*.err') \ + + glob.glob("./files/" + libname + '*.sim') \ + + glob.glob("./files/" + libname + '*.csv') \ + + glob.glob("./files/" + libname + '*.json') \ + + glob.glob("./files/" + libname + '*.html'): + try: + print("copy: " + file) + shutil.copy2(file, libFilesPath) + except: + print("-- problem during file copy... maybe the file is still hooked by a process... :" + file) + pass if clean: - for g in ["*.o","*.so","*.h","*.c","*.cpp","*.simsuccess","*.conf.json","*.tmpfiles","*.log","*.libs","OMCpp*","*.fmu*","temp_*"]: + for g in ["*.o","*.so","*.h","*.c","*.cpp","*.simsuccess","*.conf.json","*.tmpfiles","*.log","*.libs","OMCpp*","*.fmu*","temp_*", "*.exe", "HelloWorld.bat", "*.makefile", "*.mat","*.xml", "*.bin", "*.json"]: for f in glob.glob(g): - if os.path.isdir(f): - rmtree(f) - elif os.path.exists(f): - os.unlink(f) -if clean: - rmtree("files/") + try: + if os.path.isdir(f): + rmtree(f) + elif os.path.exists(f): + os.unlink(f) + except: + print("-- problem during clean of: " + f) + pass + +if clean and (result_location == "" or (not isWin and not noSync)): + try: + rmtree("files/") + except: + print("-- problem during removing of ./files dir") # Do not commit until we have generated and uploaded the reports -conn.commit() -conn.close() +db.commit() +db.release() +db.close() + +print("all tests done ...") diff --git a/testmodel.py b/testmodel.py index 5034e10..00e59a6 100755 --- a/testmodel.py +++ b/testmodel.py @@ -2,10 +2,11 @@ # -*- coding: utf-8 -*- import argparse, os, sys, signal, threading, psutil, subprocess, shutil +from asyncio.subprocess import STDOUT import simplejson as json from monotonic import monotonic from OMPython import FindBestOMCSession, OMCSession, OMCSessionZMQ -import shared +import shared, glob parser = argparse.ArgumentParser(description='OpenModelica library testing tool helper (single model)') parser.add_argument('config') @@ -14,14 +15,32 @@ parser.add_argument('--docker') parser.add_argument('--dockerExtraArgs') parser.add_argument('--corba', action="store_true", default=False) +parser.add_argument('--win', action="store_true", help="Windows mode", default=False) +parser.add_argument('--msysEnvironment', help="MSYS2 Environment (ucrt64|mingw64)", default='ucrt64') +parser.add_argument('--addmsl', action="store_true", help="add the MSL path to the OPENMODELICAPATH if the MSL is not detected in the libraries path", default=False) args = parser.parse_args() config = args.config ompython_omhome = args.ompython_omhome -libraries = args.libraries +libraries = args.libraries.replace("\\","/") docker = args.docker if args.docker else None dockerExtraArgs = args.dockerExtraArgs.split(" ") if args.dockerExtraArgs else [] corbaStyle = args.corba +isWin = args.win +msysEnvironment = args.msysEnvironment +addmsl = args.addmsl + +# our OMPython sessions +omc = None +omc_new = None + +# add openmodelica libraries path if the Modelica libraries are not found in the libraries path +MSLpath = '' +if addmsl and len(glob.glob('Modelica *', root_dir=libraries)) == 0: + if isWin: + MSLpath = ';' + os.path.normpath(os.path.join(os.environ.get('APPDATA'), '.openmodelica', 'libraries')).replace('\\','/') + else: + MSLpath = ':' + os.path.normpath(os.path.join(os.environ.get('HOME'), '.openmodelica', 'libraries')) try: os.mkdir("files") @@ -31,36 +50,110 @@ class TimeoutError(Exception): pass +runningPhase = None +"""What is running now, as (key of execstat, when it started). + +A command the watchdog has to kill never returns to its caller - +sendExpressionTimeout ends the process itself - so the caller's own timeout +handler does not run and the phase reports no time at all. Recording it here +instead covers every way out, because they all write the results first. +""" + +def phaseStarts(key): + global runningPhase + runningPhase = (key, monotonic()) + +def phaseEnded(): + global runningPhase + runningPhase = None + def writeResult(): + if runningPhase is not None: + (key, started) = runningPhase + # Only if the phase did not get to report its own time. + if not execstat.get(key): + execstat[key] = monotonic() - started with open(statFile, 'w') as fp: json.dump(execstat, fp) fp.flush() os.fsync(fp.fileno()) -def writeResultAndExit(exitStatus): +startJob=monotonic() + +def quit_omc(omc): + if omc is None: + return omc + try: + omc.sendExpression("quit()") + except: + pass + try: + del omc + except: + pass + omc = None + return omc + +def writeResultAndExit(exitStatus, useOsExit=False, omc=None, omc_new=None): writeResult() - sys.exit(exitStatus) - + print("Calling exit ...") + with open(errFile, 'a+') as fp: + if useOsExit: + msg = "[Calling os._exit(%s), Time elapsed: %s]\n" + else: + msg = "[Calling sys.exit(%s), Time elapsed: %s]\n" + fp.write(msg % (exitStatus, monotonic()-startJob)) + fp.flush() + sys.stdout.flush() + omc = quit_omc(omc) + omc_new = quit_omc(omc_new) + if useOsExit: + os._exit(exitStatus) + else: + sys.exit(exitStatus) + def sendExpressionTimeout(omc, cmd, timeout): with open(errFile, 'a+') as fp: - fp.write(cmd + "\n") + fp.write("%s [Timeout %s]\n" % (cmd, timeout)) def target(res): try: + ignore = omc.sendExpression("alarm(%s)" % timeout) res[0] = omc.sendExpression(cmd) + with open(errFile, 'a+') as fp: + fp.write(omc.sendExpression('OpenModelica.Scripting.getErrorString()', parsed = False)) + elapsed = omc.sendExpression("alarm(0)") + with open(errFile, 'a+') as fp: + fp.write("[Timeout remaining time %s]\n" % elapsed) except Exception as e: res[1] = cmd + " " + str(e) res=[None,None] thread = threading.Thread(target=target, args=(res,)) thread.start() - thread.join(timeout) + # Poll instead of a single join: if omc dies (crash, ulimit, ...) the thread is + # stuck in a ZMQ receive that never returns, so waiting out the timeout and then + # exiting normally would hang forever on that non-daemon thread + deadline = monotonic() + timeout + while thread.is_alive() and monotonic() < deadline: + thread.join(1) + status = omc._omc_process.poll() + if thread.is_alive() and status is not None: + with open(errFile, 'a+') as fp: + fp.write("OMC exited with status %s while running: %s\n" % (status, cmd)) + try: + with open(os.path.normpath(omc._omc_log_file.name)) as omcLog: + for line in omcLog: + fp.write(line) + except IOError: + pass + writeResultAndExit(0, True, omc, omc_new) if thread.is_alive(): with open(errFile, 'a+') as fp: fp.write("Thread is still alive.\n") if omc._omc_process.poll() is not None: fp.write("OMC died, but the thread is still running? This will end badly. The log-file of omc:\n") - with open(omc._omc_log_file.name) as omcLog: + with open(os.path.normpath(omc._omc_log_file.name)) as omcLog: for line in omcLog: fp.write(line) print("OMC died, but the thread is still running? This will end badly.\n") @@ -68,6 +161,8 @@ def target(res): try: os.kill(process.pid, signal.SIGINT) except OSError: + with open(errFile, 'a+') as fp: + fp.write("Could not SIGINT process: %s.\n" % process.pid) pass thread.join(2) if thread.is_alive(): @@ -75,27 +170,31 @@ def target(res): try: os.kill(process.pid, signal.SIGKILL) except OSError: + with open(errFile, 'a+') as fp: + fp.write("Could not SIGKILL process: %s.\n" % process.pid) pass with open(errFile, 'a+') as fp: fp.write("Aborted the command.\n") - writeResultAndExit(0) + writeResultAndExit(0, True, omc, omc_new) if res[1] is None: res[1] = "" if res[1] is not None: raise TimeoutError(res[1]) return res[0] -def checkOutputTimeout(cmd, timeout): +def checkOutputTimeout(cmd, timeout, conf=None): with open(errFile, 'a+') as fp: - fp.write(cmd + "\n") + fp.write("%s [Timeout %s]\n" % (cmd, timeout)) def target(res): try: - res[0] = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT).decode().strip() + env = os.environ.copy() + # add the environmentSimulation to the environment + if conf: + for e in conf["environmentSimulation"]: + env[e[0]] = e[1] + res[0] = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, env = env).decode().strip() except subprocess.CalledProcessError as e: - try: - outputStr = str(e.output).decode("utf-8") - except: - outputStr = str(e.output) + outputStr = e.output.decode("utf-8","backslashreplace") res[1] = cmd + " " + outputStr except Exception as e: res[1] = cmd + " " + str(e) @@ -110,6 +209,8 @@ def target(res): try: os.killpg(process.pid, signal.SIGINT) except OSError: + with open(errFile, 'a+') as fp: + fp.write("Could not SIGINT process: %s.\n" % process.pid) pass thread.join(2) if thread.is_alive(): @@ -117,6 +218,8 @@ def target(res): try: os.kill(process.pid, signal.SIGKILL) except OSError: + with open(errFile, 'a+') as fp: + fp.write("Could not SIGKILL process: %s.\n" % process.pid) pass thread.join() if res[1] is None: @@ -133,9 +236,14 @@ def target(res): "templates":None, "build":None, "sim":None, + "simcold":None, "diff":None, - "phase":0 + "phase":0, + # One entry per FMI simulator beyond the first, which reports itself in the + # keys above; see configs/fmi-simulators.json. + "simulators":{} } +simulators = execstat["simulators"] with open(config) as fp: conf = json.load(fp) @@ -149,9 +257,9 @@ def target(res): dockerExtraArgs = dockerExtraArgs + ["-w", conf["fileName"]] -errFile="../files/%s.err" % conf["fileName"] -simFile="../files/%s.sim" % conf["fileName"] -statFile="../files/%s.stat.json" % conf["fileName"] +errFile=os.path.normpath("../files/%s.err" % conf["fileName"]) +simFile=os.path.normpath("../files/%s.sim" % conf["fileName"]) +statFile=os.path.normpath("../files/%s.stat.json" % conf["fileName"]) try: os.unlink(errFile) except OSError: @@ -164,10 +272,23 @@ def target(res): with open(errFile, 'a+') as fp: fp.write("Running: %s\n" % " ".join(sys.argv)) -if conf["simCodeTarget"] not in ["Cpp","C"]: +if conf["simCodeTarget"] not in ["Cpp","C","wasm-jit"]: with open(errFile, 'a+') as fp: fp.write("Unknown simCodeTarget in %s" % conf["simCodeTarget"]) writeResultAndExit(1) +# wasm-jit builds no makefile and no executable; the model is JIT-compiled inside +# the omc that translated it and simulated there via simulate(resimulateExecutable=) +isWasmJit = conf["simCodeTarget"]=="wasm-jit" +# --nobuildmodel: one simulate() instead of translateModel()+resimulate, so omc +# reports the build/simulation split itself +useSimulate = isWasmJit and conf.get("noBuildModel") and not conf.get("fmi") +# --coldhot: simulate again in the same session, where the module is already +# compiled, and report that run instead +useColdHot = isWasmJit and conf.get("coldHot") and not conf.get("fmi") +if isWasmJit and conf.get("fmi"): + with open(errFile, 'a+') as fp: + fp.write("FMI export is not supported for simCodeTarget=wasm-jit") + writeResultAndExit(0) if conf["simCodeTarget"]=="Cpp" and not conf["haveCppRuntime"]: with open(errFile, 'a+') as fp: fp.write("C++ runtime not supported in this installation (HelloWorld failed)") @@ -196,12 +317,12 @@ def createOmcSessionNew(): omc = createOmcSession() omc_new = createOmcSessionNew() -cmd = 'setCommandLineOptions("%s")' % conf["single_thread_cmd"] +cmd = 'setCommandLineOptions("%s")' % conf["omc_thread_cmd"] if not omc.sendExpression(cmd): raise Exception('Could not send %s' % cmd) try: - os.unlink("%s.tmpfiles" % conf["fileName"]) + os.unlink(os.path.normpath("%s.tmpfiles" % conf["fileName"])) except: pass #cmd = 'setCommandLineOptions("--running-testsuite=%s.tmpfiles")' % conf["fileName"] @@ -214,16 +335,19 @@ def createOmcSessionNew(): outputFormat="mat" referenceVars=[] +numberOfIntervalsInReference = 0 referenceFile = conf.get("referenceFile") or "" if referenceFile != "": try: - compSignals = os.path.join(os.path.dirname(referenceFile),"comparisonSignals.txt") + compSignals = os.path.normpath(os.path.join(os.path.dirname(referenceFile),"comparisonSignals.txt")) if os.path.exists(compSignals): referenceVars=[s.strip() for s in open(compSignals).readlines() if (s.strip() != "")] # s.strip().lower() != "time" and ??? I guess we should check time variable... print(referenceVars) else: referenceVars=omc_new.sendExpression('readSimulationResultVars("%s", readParameters=true, openmodelicaStyle=true)' % referenceFile) variableFilter="|".join([v.replace("[",".").replace("]",".").replace("(",".").replace(")",".").replace('"',".") for v in referenceVars]) + # get the number of intervals from the file + numberOfIntervalsInReference = omc_new.sendExpression('readSimulationResultSize("%s")' % referenceFile) emit_protected="-emit_protected" except: referenceFile="" @@ -253,7 +377,7 @@ def createOmcSessionNew(): cflags += " " + conf["optlevel"] omc.sendExpression(str("setCFlags(\"%s\")" % cflags), parsed = False) -omc.sendExpression('setModelicaPath("%s")' % libraries, parsed = False) +omc.sendExpression('setModelicaPath("%s")' % (libraries+MSLpath,), parsed = False) if conf.get("ulimitMemory"): # Use at most 80% of the vmem for the GC heap; some memory will be used for other purposes than the GC itself @@ -263,12 +387,10 @@ def createOmcSessionNew(): def loadModels(omc, conf): for f in conf["loadFiles"]: if not sendExpressionTimeout(omc, 'loadFile("%s", uses=false)' % f, conf["ulimitLoadModel"]): - with open(errFile, 'a+') as fp: - fp.write(omc.sendExpression('OpenModelica.Scripting.getErrorString()')) - writeResultAndExit(0) + writeResultAndExit(0, False, omc, omc_new) loadedFiles = sorted(omc.sendExpression("{getSourceFile(cl) for cl in getClassNames()}")) - if conf["loadFiles"] != loadedFiles: - print("Loaded the wrong files. Expected:\n%s\nActual:\n%s", ("\n".join(conf["loadFiles"]), "\n".join(loadedFiles))) + if sorted(conf["loadFiles"]) != loadedFiles: + print("Loaded the wrong files. Expected:\n%s\nActual:\n%s" % ("\n".join(sorted(conf["loadFiles"])), "\n".join(loadedFiles))) sys.exit(1) newOMLoaded = False def loadLibraryInNewOM(): @@ -277,7 +399,7 @@ def loadLibraryInNewOM(): newOMLoaded = True # Broken/old getSimulationOptions; use new one (requires parsing again) assert(ompython_omhome!="") - assert(omc_new.sendExpression('setModelicaPath("%s")' % libraries)) + assert(omc_new.sendExpression('setModelicaPath("%s")' % (libraries+MSLpath,))) loadModels(omc_new, conf) start=monotonic() @@ -287,7 +409,7 @@ def loadLibraryInNewOM(): execstat["parsing"]=monotonic()-start with open(errFile, 'a+') as fp: fp.write("Timeout error for cmd: %s\n%s"%(cmd,str(e))) - writeResultAndExit(0) + writeResultAndExit(0, True, omc, omc_new) execstat["parsing"]=monotonic()-start try: @@ -314,67 +436,97 @@ def sendExpressionOldOrNew(cmd): loadLibraryInNewOM() return omc_new.sendExpression(cmd) +haveFlagCheckModel=False +def wasmJitAcceptsFlag(flagVal): + # There is no HelloWorld executable to probe: the wasm-jit runtime lives in + # omc, so ask it directly whether a trivial model still simulates. + global haveFlagCheckModel + if not haveFlagCheckModel: + sendExpressionOldOrNew('loadString("model OMLibTestFlagCheck Real x(start = 1, fixed = true); equation der(x) = -x; end OMLibTestFlagCheck;")') + haveFlagCheckModel=True + return bool((sendExpressionOldOrNew('simulate(OMLibTestFlagCheck,simflags="%s")' % flagVal) or {}).get("resultFile")) + annotationSimFlags="" -(startTime,stopTime,tolerance,numberOfIntervals,stepSize)=sendExpressionOldOrNew('getSimulationOptions(%s,defaultTolerance=%s,defaultNumberOfIntervals=2500)' % (conf["modelName"],conf["default_tolerance"])) -if conf["simCodeTarget"]=="C" and sendExpressionOldOrNew('classAnnotationExists(%s, __OpenModelica_simulationFlags)' % conf["modelName"]): +(startTime,stopTime,tolerance,numberOfIntervals,stepSize)=sendExpressionOldOrNew('getSimulationOptions(%s,defaultTolerance=%s,defaultNumberOfIntervals=%s)' % (conf["modelName"], conf["defaultTolerance"], max(conf["defaultNumberOfIntervals"], numberOfIntervalsInReference))) +if conf["simCodeTarget"] in ("C","wasm-jit") and sendExpressionOldOrNew('classAnnotationExists(%s, __OpenModelica_simulationFlags)' % conf["modelName"]): for flag in sendExpressionOldOrNew('getAnnotationNamedModifiers(%s,"__OpenModelica_simulationFlags")' % conf["modelName"]): if flag=="The searched annotation name not found": # Old, stupid API continue val=sendExpressionOldOrNew('getAnnotationModifierValue(%s,"__OpenModelica_simulationFlags","%s")' % (conf["modelName"],flag)) flagVal=" -noemit -%s=%s" % (flag,val) - if shared.simulationAcceptsFlag(flagVal, checkOutput=False, cwd=".."): + if wasmJitAcceptsFlag("-%s=%s" % (flag,val)) if isWasmJit else shared.simulationAcceptsFlag(flagVal, checkOutput=False, cwd="..", isWin=isWin): annotationSimFlags+=" -%s=%s" % (flag,val) else: with open(errFile, 'a+') as fp: - fp.write("Ignoring simflag %s since it seems broken on HelloWorld\n" % flagVal) + fp.write("Ignoring simflag %s since the simulation runtime does not accept it\n" % flagVal) + +def simulateCmd(resimulate): + simflags = ("%s %s %s -lv LOG_STATS" % (annotationSimFlags,conf["simFlags"],emit_protected)).strip() + return 'simulate(%s,startTime=%g,stopTime=%g,tolerance=%g,numberOfIntervals=%d,outputFormat="%s",variableFilter="%s",fileNamePrefix="%s",simflags="%s"%s)' % (conf["modelName"],startTime,stopTime,tolerance,numberOfIntervals,outputFormat,variableFilter,conf["fileName"],simflags,(',resimulateExecutable="%s"' % conf["fileName"]) if resimulate else "") # TODO: Detect and handle the case where RT_CLOCK is not available in OMC total_before = omc.sendExpression("OpenModelica.Scripting.Internal.Time.timerTock(OpenModelica.Scripting.Internal.Time.RT_CLOCK_SIMULATE_TOTAL)") start=monotonic() +timeout = conf["ulimitOmc"] +# If the command has to be killed there is no way to tell which phase it was in, +# so its time is charged to what the command itself is: building an FMU to the +# build, simulating to the simulation, translating to the front end - which is +# also the phase such a model is reported as having failed in. if conf.get("fmi"): cmd='"" <> buildModelFMU(%s,fileNamePrefix="%s",fmuType="%s",version="%s",platforms={"static"})' % (conf["modelName"],conf["fileName"].replace(".","_"),conf["fmuType"],conf["fmi"]) + timedPhase = "build" +elif useSimulate: + cmd=simulateCmd(resimulate=False) + timeout = conf["ulimitOmc"] + conf["ulimitExe"] + timedPhase = "sim" else: - cmd='translateModel(%s,tolerance=%g,outputFormat="%s",numberOfIntervals=%d,variableFilter="%s",fileNamePrefix="%s")' % (conf["modelName"],tolerance,outputFormat,2*numberOfIntervals,variableFilter,conf["fileName"]) + cmd='translateModel(%s,tolerance=%g,outputFormat="%s",numberOfIntervals=%d,variableFilter="%s",fileNamePrefix="%s")' % (conf["modelName"],tolerance,outputFormat,numberOfIntervals,variableFilter,conf["fileName"]) + timedPhase = "frontend" with open(errFile, 'a+') as fp: fp.write("Running command: %s\n"%(cmd)) try: - res=sendExpressionTimeout(omc, cmd, conf["ulimitOmc"]) + phaseStarts(timedPhase) + res=sendExpressionTimeout(omc, cmd, timeout) + phaseEnded() except TimeoutError as e: - execstat["frontend"]=monotonic()-start + execstat[timedPhase]=monotonic()-start with open(errFile, 'a+') as fp: fp.write("Timeout error for cmd: %s\n%s"%(cmd,str(e))) try: - name = omc._omc_log_file.name + name = os.path.normpath(omc._omc_log_file.name) del omc with open(name,"r") as fp2: fp.write("\n\nOMC output: %s" % fp2.read().decode().strip()) except: pass - writeResultAndExit(0) + writeResultAndExit(0, omc, omc_new) # See which translateModel phases completed execTimeTranslateModel=monotonic()-start -err=omc.sendExpression("OpenModelica.Scripting.getErrorString()") -total = omc.sendExpression("OpenModelica.Scripting.Internal.Time.timerTock(OpenModelica.Scripting.Internal.Time.RT_CLOCK_SIMULATE_TOTAL)")-total_before -buildmodel= omc.sendExpression("OpenModelica.Scripting.Internal.Time.timerTock(OpenModelica.Scripting.Internal.Time.RT_CLOCK_BUILD_MODEL)") -templates= omc.sendExpression("OpenModelica.Scripting.Internal.Time.timerTock(OpenModelica.Scripting.Internal.Time.RT_CLOCK_TEMPLATES)") -simcode = omc.sendExpression("OpenModelica.Scripting.Internal.Time.timerTock(OpenModelica.Scripting.Internal.Time.RT_CLOCK_SIMCODE)") -backend = omc.sendExpression("OpenModelica.Scripting.Internal.Time.timerTock(OpenModelica.Scripting.Internal.Time.RT_CLOCK_BACKEND)") -frontend = omc.sendExpression("OpenModelica.Scripting.Internal.Time.timerTock(OpenModelica.Scripting.Internal.Time.RT_CLOCK_FRONTEND)") +simres = None +buildFailed = False +if useSimulate: + simres = res or {} + # A failed translate/build is only reported in the messages of the record; the + # translation clocks below say which of the two it was + buildFailed = (simres.get("messages") or "").startswith("Failed to build model") + res = True +err = omc.sendExpression("OpenModelica.Scripting.getErrorString()") +total = omc.sendExpression("OpenModelica.Scripting.Internal.Time.timerTock(OpenModelica.Scripting.Internal.Time.RT_CLOCK_SIMULATE_TOTAL)")-total_before +buildmodel = omc.sendExpression("OpenModelica.Scripting.Internal.Time.timerTock(OpenModelica.Scripting.Internal.Time.RT_CLOCK_BUILD_MODEL)") +templates = omc.sendExpression("OpenModelica.Scripting.Internal.Time.timerTock(OpenModelica.Scripting.Internal.Time.RT_CLOCK_TEMPLATES)") +simcode = omc.sendExpression("OpenModelica.Scripting.Internal.Time.timerTock(OpenModelica.Scripting.Internal.Time.RT_CLOCK_SIMCODE)") +backend = omc.sendExpression("OpenModelica.Scripting.Internal.Time.timerTock(OpenModelica.Scripting.Internal.Time.RT_CLOCK_BACKEND)") +frontend = omc.sendExpression("OpenModelica.Scripting.Internal.Time.timerTock(OpenModelica.Scripting.Internal.Time.RT_CLOCK_FRONTEND)") writeResult() -try: - omc.sendExpression("quit()") -except: - pass -try: - del omc -except: - pass +if not isWasmJit or (useSimulate and not useColdHot): + # wasm-jit keeps the translated model in this session; it is needed to simulate + omc = quit_omc(omc) print(execTimeTranslateModel,frontend,backend) if backend != -1: @@ -406,7 +558,7 @@ def sendExpressionOldOrNew(cmd): fp.write(err) if execstat["phase"] < 4: - writeResultAndExit(0) + writeResultAndExit(0, False, omc, omc_new) start=monotonic() try: @@ -414,160 +566,311 @@ def sendExpressionOldOrNew(cmd): if res: fmuExpectedLocation = "%s.fmu" % conf["fileName"].replace(".","_") execstat["build"] = max(0.0, buildmodel) # Older versions didn't separate translate and build times - if not os.path.exists(fmuExpectedLocation): + if not os.path.exists(os.path.normpath(fmuExpectedLocation)): err += "\nFMU was not generated in the expected location: %s" % fmuExpectedLocation execstat["phase"]=4 - writeResultAndExit(0) + writeResultAndExit(0, False, omc, omc_new) execstat["phase"] = 5 + elif isWasmJit: + # Nothing to build; simulate() reports the JIT compile as timeCompile, while + # a resimulate leaves it in the simulation time + execstat["build"] = simres["timeCompile"] if useSimulate else 0.0 + execstat["phase"] = 5 + if buildFailed: + with open(errFile, 'a+') as fp: + fp.write(simres.get("messages") or "") + writeResultAndExit(0, False, omc, omc_new) else: - - res = checkOutputTimeout("make -j1 -f %s.makefile" % conf["fileName"], conf["ulimitOmc"]) + if isWin: + res = checkOutputTimeout("\"%s\\share\\omc\\scripts\\Compile.bat\" %s gcc %s parallel dynamic 24 0" % (conf["omhome"], conf["fileName"], msysEnvironment), conf["ulimitOmc"], conf) + else: + res = checkOutputTimeout("make -j%s -f %s.makefile" % (conf["procCCompile"], conf["fileName"]), conf["ulimitOmc"], conf) + execstat["build"] = monotonic()-start execstat["phase"] = 5 except TimeoutError as e: execstat["build"] = monotonic()-start with open(errFile, 'a+') as fp: fp.write(str(e)) - writeResultAndExit(0) + writeResultAndExit(0, True, omc, omc_new) writeResult() # Do the simulation -resFile = "%s_res.%s" % (conf["fileName"], outputFormat) +# The FMU is built once and simulated with every tool the job asked for, so +# that testing FMPy no longer means building the same FMU a second time. The +# tools are described in configs/fmi-simulators.json. +fmisimulator = conf.get("fmisimulator") +fmisimulators = shared.parseFmiSimulators(conf.get("fmisimulators")) if conf.get("fmi") else [] +if conf.get("fmi") and not fmisimulators and fmisimulator: + fmisimulators = shared.parseFmiSimulators([fmisimulator]) + +def resultFile(name=None): + """Where a simulator writes its results. + + Every tool writes what its entry says, and they only need to be told apart + when more than one of them runs on the same FMU. + """ + if not name: + return "%s_res.%s" % (conf["fileName"], outputFormat) + extension = shared.fmiSimulator(name)["resultExtension"] + if len(fmisimulators) < 2: + return "%s_res.%s" % (conf["fileName"], extension) + return "%s_%s_res.%s" % (conf["fileName"], name, extension) + +def artifactPrefix(name=None): + """The files a simulator's results are written to, under files/.""" + if not name or len(fmisimulators) < 2: + return os.path.abspath("../files/%s" % conf["fileName"]).replace('\\','/') + return os.path.abspath("../files/%s_%s" % (conf["fileName"], name)).replace('\\','/') + +resFile = resultFile(fmisimulators[0][0]) if fmisimulators else resultFile() + +def simulateFmu(name, command, resFile, simFile): + """Run the FMU with one simulator, writing what it says to simFile.""" + # Only tell the runs apart when more than one of them shares the directory. + suffix = "_%s" % name if len(fmisimulators) > 1 else "" + fmitmpdir = "temp_%s%s_fmu" % (conf["fileName"].replace(".","_"), suffix) + with open("%s.tmpfiles" % conf["fileName"], "a+") as fp: + fp.write("%s\n" % fmitmpdir) + cmd = shared.fmiSimulatorCommand(name, command, + fmu="%s.fmu" % conf["fileName"].replace(".","_"), + result=resFile, + requestedResult=resFile if outputFormat != "empty" else "", + tempDir=fmitmpdir, startTime=startTime, stopTime=stopTime, + tolerance=tolerance, timeout=conf["ulimitExe"], + stepSize=stepSize) + with open(simFile,"w") as fp: + fp.write("%s\n" % cmd) + pipe = "%s%s" % (conf["fileName"], suffix) + return checkOutputTimeout("(rm -f %s.pipe ; mkfifo %s.pipe ; head -c 1048576 < %s.pipe >> %s & %s > %s.pipe 2>&1)" + % (pipe,pipe,pipe,simFile,cmd,pipe), 1.05*conf["ulimitExe"], conf) + +def simElapsed(): + # omc's own time: the wall clock here covers the wrong run for both flags + if useSimulate or useColdHot: + return (simres or {}).get("timeSimulation") or 0.0 + return monotonic()-start start=monotonic() +# Set when the first FMI simulator fails and there are others waiting for the +# same FMU, so that its result file is not compared against the reference. +firstSimulatorFailed = False try: # TODO: Timeout more reliably... if conf.get("fmi"): - if not conf.get("fmisimulator"): + if not fmisimulators: with open(simFile,"w") as fp: - fp.write("OMSimulator not available\n") - writeResultAndExit(0) - fmitmpdir = "temp_%s_fmu" % conf["fileName"].replace(".","_") - with open("%s.tmpfiles" % conf["fileName"], "a+") as fp: - fp.write("%s\n" % fmitmpdir) - cmd = "%s --tempDir=%s --startTime=%g --stopTime=%g --timeout=%g --tolerance=%g %s.fmu" % (("-r=%s" % resFile) if outputFormat != "empty" else "",fmitmpdir,startTime,stopTime,conf["ulimitExe"],tolerance,conf["fileName"].replace(".","_")) + fp.write("No FMI simulator available\n") + writeResultAndExit(0, False, omc, omc_new) + (name, command) = fmisimulators[0] + res = simulateFmu(name, command, resFile, simFile) + elif isWasmJit: + if not useSimulate: + cmd = simulateCmd(resimulate=True) with open(simFile,"w") as fp: - fp.write("OMSimulator %s\n" % cmd) - #res = checkOutputTimeout("%s %s >> %s 2>&1" % (conf["fmisimulator"],cmd,simFile), conf["ulimitExe"]) - res = checkOutputTimeout("(rm -f %s.pipe ; mkfifo %s.pipe ; head -c 1048576 < %s.pipe >> %s & %s %s > %s.pipe 2>&1)" % (conf["fileName"],conf["fileName"],conf["fileName"],simFile,conf["fmisimulator"],cmd,conf["fileName"]), 1.05*conf["ulimitExe"]) + fp.write("startTime=%g\nstopTime=%g\ntolerance=%g\nnumberOfIntervals=%d\nstepSize=%g\n" % (startTime,stopTime,tolerance,numberOfIntervals,stepSize)) + fp.write("wasm-jit simulation: %s\n" % cmd) + if not useSimulate: + simres = sendExpressionTimeout(omc, cmd, conf["ulimitExe"]) or {} + with open(simFile,"a+") as fp: + fp.write(simres.get("messages") or "") + if not simres.get("resultFile"): + execstat["sim"] = simElapsed() + writeResultAndExit(0, False, omc, omc_new) + if useColdHot: + execstat["simcold"] = simElapsed() + cmd = simulateCmd(resimulate=True) + with open(simFile,"a+") as fp: + fp.write("wasm-jit hot simulation: %s\n" % cmd) + hotres = sendExpressionTimeout(omc, cmd, conf["ulimitExe"]) or {} + with open(simFile,"a+") as fp: + fp.write(hotres.get("messages") or "") + if hotres.get("resultFile"): + simres = hotres + else: + with open(errFile, 'a+') as fp: + fp.write("The hot simulation failed; keeping the cold time\n") else: - cmd = ("./%s %s %s %s" % (conf["fileName"],conf["simFlags"],annotationSimFlags,emit_protected)).strip() + if isWin: + cmd = (".\\%s.bat %s %s %s" % (conf["fileName"],annotationSimFlags,conf["simFlags"],emit_protected)).strip() + else: + cmd = ("./%s %s %s %s" % (conf["fileName"],annotationSimFlags,conf["simFlags"],emit_protected)).strip() + if conf["simCodeTarget"]=="C": cmd = cmd + " -lv LOG_STATS" + executable = os.path.normpath("%s.bat" % conf["fileName"] if isWin else conf["fileName"]) + if not os.path.exists(executable): + with open(errFile, 'a+') as fp: + fp.write("The simulation executable %s does not exist\n" % executable) + execstat["sim"] = monotonic()-start + writeResultAndExit(0, False, omc, omc_new) with open(simFile,"w") as fp: + fp.write("Environment - simulationEnvironment:\n") + for e in conf["environmentSimulation"]: + fp.write("%s = %s\n" % (e[0], e[1])) fp.write("startTime=%g\nstopTime=%g\ntolerance=%g\nnumberOfIntervals=%d\nstepSize=%g\n" % (startTime,stopTime,tolerance,numberOfIntervals,stepSize)) fp.write("Regular simulation: %s\n" % cmd) - res = checkOutputTimeout("(rm -f %s.pipe ; mkfifo %s.pipe ; head -c 1048576 < %s.pipe >> %s & %s > %s.pipe 2>&1)" % (conf["fileName"],conf["fileName"],conf["fileName"],simFile,cmd,conf["fileName"]), conf["ulimitExe"]) - execstat["sim"] = monotonic()-start + if isWin: + res = checkOutputTimeout("%s >> %s" % (cmd,simFile), conf["ulimitExe"], conf) + else: + res = checkOutputTimeout("(rm -f %s.pipe ; mkfifo %s.pipe ; head -c 1048576 < %s.pipe >> %s & %s > %s.pipe 2>&1)" % (conf["fileName"],conf["fileName"],conf["fileName"],simFile,cmd,conf["fileName"]), conf["ulimitExe"], conf) + execstat["sim"] = simElapsed() execstat["phase"] = 6 except TimeoutError as e: execstat["sim"] = monotonic()-start - writeResultAndExit(0) + # checkOutputTimeout raises TimeoutError for a command that fails as well as + # for one that runs out of time, so this covers both. + if len(fmisimulators) > 1: + # The FMU is built and the other simulators are about to run it. What the + # first one did says nothing about them, so record the failure and let them + # have their turn. Ending the model here would write them all down as having + # failed at this phase without ever being started, which is what the loop + # below already avoids for a failure in any simulator but the first. + firstSimulatorFailed = True + with open(errFile, 'a+') as fp: + fp.write("%s failed or timed out simulating the FMU; the other simulators still get to run it\n" + % fmisimulators[0][0]) + else: + writeResultAndExit(0, True, omc, omc_new) -if referenceFile=="": - writeResultAndExit(0) -if len(referenceVars)==0: - execstat["diff"] = {"time":0.0, "vars":[], "numCompared":0} - execstat["phase"]=7 - writeResultAndExit(0) +def verifyAgainstReference(resFile, prefix, stat): + """Compare one simulation result against the reference file. -# Check the reference file... + Fills stat["diff"] and stat["phase"] exactly as the single simulator code + did, but returns instead of ending the run, so that the other simulators + of the same FMU can be verified too. + """ + if referenceFile=="": + return + if len(referenceVars)==0: + stat["diff"] = {"time":0.0, "vars":[], "numCompared":0} + stat["phase"]=7 + return -prefix = "../files/%s.diff" % conf["fileName"] -if not os.path.exists(resFile): - with open(errFile, 'a+') as fp: - fp.write("TODO: How the !@#!# did the simulation report success but simulation result %s does not exist to compare? outputFormat=%s" % (resFile,outputFormat)) - writeResultAndExit(0) + if not os.path.exists(os.path.normpath(resFile)): + with open(errFile, 'a+') as fp: + fp.write("TODO: How the !@#!# did the simulation report success but simulation result %s does not exist to compare? outputFormat=%s" % (resFile,outputFormat)) + return -start=monotonic() -if False and conf["simCodeTarget"] in ["Cpp"]: # This is a work-around for older C++ runtime not supporting variable filters. We don't really need it for master, so let's no use it. - if not sendExpressionTimeout(omc_new, 'filterSimulationResults("%s", "updated%s", vars={%s}, removeDescription=false, hintReadAllVars=false)' % (resFile, resFile, ", ".join(['"%s"' % s for s in referenceVars])), conf["ulimitOmc"]): + start=monotonic() + if False and conf["simCodeTarget"] in ["Cpp"]: # This is a work-around for older C++ runtime not supporting variable filters. We don't really need it for master, so let's no use it. + if not sendExpressionTimeout(omc_new, 'filterSimulationResults("%s", "updated%s", vars={%s}, removeDescription=false, hintReadAllVars=false)' % (resFile, resFile, ", ".join(['"%s"' % s for s in referenceVars])), conf["ulimitOmc"]): + with open(errFile, 'a+') as fp: + fp.write("Failed to filter simulation results. Took time: %.2f\n" % (monotonic()-start)) + return + os.remove(resFile) + os.rename("updated" + resFile, resFile) with open(errFile, 'a+') as fp: - fp.write("Failed to filter simulation results. Took time: %.2f\n" % (monotonic()-start)) - writeResultAndExit(0) - os.remove(resFile) - os.rename("updated" + resFile, resFile) - with open(errFile, 'a+') as fp: - fp.write("Filtered simulation results in time: %.2f\n" % (monotonic()-start)) -start=monotonic() -try: - (referenceOK,diffVars) = sendExpressionTimeout(omc_new, 'diffSimulationResults("%s","%s","%s",relTol=%g,relTolDiffMinMax=%g,rangeDelta=%g)' % - (resFile, referenceFile, prefix, conf["reference_reltol"],conf["reference_reltolDiffMinMax"], conf["reference_rangeDelta"]), conf["ulimitOmc"]) -except TimeoutError as e: - with open(errFile, 'a+') as fp: - fp.write("Timeout error for diffSimulationResults") - writeResultAndExit(0) + fp.write("Filtered simulation results in time: %.2f\n" % (monotonic()-start)) + start=monotonic() + try: + (referenceOK,diffVars) = sendExpressionTimeout(omc_new, 'diffSimulationResults("%s","%s","%s",relTol=%g,relTolDiffMinMax=%g,rangeDelta=%g)' % + (resFile, referenceFile, prefix, conf["reference_reltol"],conf["reference_reltolDiffMinMax"], conf["reference_rangeDelta"]), conf["ulimitOmc"]) + except TimeoutError as e: + with open(errFile, 'a+') as fp: + fp.write("Timeout error for diffSimulationResults") + return -execstat["diff"] = {"time":monotonic()-start, "vars":[], "numCompared":len(referenceVars)} -if len(diffVars)==0 and referenceOK: - execstat["phase"]=7 - with open(errFile, 'a+') as fp: - fp.write("Reference file matches\n") -else: - with open(errFile, 'a+') as fp: - fp.write(omc_new.sendExpression('OpenModelica.Scripting.getErrorString()', parsed = False)) - fp.write("\nVariables in the reference:" ) - fp.write(",".join(referenceVars)+"\n") - resVars=omc_new.sendExpression('readSimulationResultVars("%s", readParameters=true, openmodelicaStyle=true)' % resFile) - fp.write("\nVariables in the result:" ) - fp.write(",".join(resVars)+"\n") - diffFiles = [prefix + "." + var for var in diffVars] - execstat["diff"]["vars"]=diffVars - - # Create a file containing only the calibrated variables, for easy display - lstfiles = "\n".join(['
  • %s (javascript) (csv)
  • ' % (str.split(str(f),".diff.",1)[1],str(os.path.basename(f)),str(os.path.basename(f))) for f in diffFiles]) - with open(prefix+".html", 'w') as fp: - fp.write("

    %s differences from the reference file

    startTime: %g

    stopTime: %g

    Simulated using tolerance: %g

      %s
    " % (conf["modelName"], startTime, stopTime, tolerance, lstfiles)); - for var in diffVars: - if "/" in var: - continue # Quoted identifier, or possibly an error message... Either way, avoid crapping out below - with open(prefix+"."+var+".html", 'w') as fp: - fp.write(""" - - - - - -
    -

    - - - - - - - - - - - -Parameters used for the comparison: Relative tolerance %g (local), %g (relative to max-min). Range delta %g.

    - - -""" % (tolerance, conf["reference_reltolDiffMinMax"], conf["reference_rangeDelta"], os.path.basename(prefix + "." + var + ".csv"), var)) + stat["diff"] = {"time":monotonic()-start, "vars":[], "numCompared":len(referenceVars)} + if len(diffVars)==0 and referenceOK: + stat["phase"]=7 + with open(errFile, 'a+') as fp: + fp.write("Reference file matches\n") + else: + with open(errFile, 'a+') as fp: + fp.write(omc_new.sendExpression('OpenModelica.Scripting.getErrorString()', parsed = False)) + fp.write("\nVariables in the reference:" ) + fp.write(",".join(referenceVars)+"\n") + resVars=omc_new.sendExpression('readSimulationResultVars("%s", readParameters=true, openmodelicaStyle=true)' % resFile) + fp.write("\nVariables in the result:" ) + fp.write(",".join(resVars)+"\n") + diffFiles = [prefix + "." + var for var in diffVars] + stat["diff"]["vars"]=diffVars + + # Create a file containing only the calibrated variables, for easy display + lstfiles = "\n".join(['
  • %s (javascript) (csv)
  • ' % (str.split(str(f),".diff.",1)[1],str(os.path.basename(f)),str(os.path.basename(f))) for f in diffFiles]) + with open(prefix+".html", 'w') as fp: + fp.write('

    %s differences from the reference file

    startTime: %g

    stopTime: %g

    Simulated using tolerance: %g

      %s
    ' % (conf["modelName"], startTime, stopTime, tolerance, lstfiles)) + for var in diffVars: + if "/" in var: + continue # Quoted identifier, or possibly an error message... Either way, avoid crapping out below + with open(prefix+"."+var+".html", 'w') as fp: + fp.write(""" + + + + + +
    +

    + + + + + + + + + + + + Parameters used for the comparison: Relative tolerance %g (local), %g (relative to max-min). Range delta %g.

    + + + """ % (tolerance, conf["reference_reltolDiffMinMax"], conf["reference_rangeDelta"], os.path.basename(prefix + "." + var + ".csv"), var)) + + +# The first simulator's results are the ones every non-FMI code path expects. +# There is nothing to compare when it never produced any. +if not firstSimulatorFailed: + verifyAgainstReference(resFile, artifactPrefix(fmisimulators[0][0] if fmisimulators else None) + ".diff", execstat) + +# The FMU is built; every other simulator the job asked for is now only a +# simulation and a comparison. A tool that times out or fails takes its own +# results down with it and leaves the others alone - the first one included, +# see the TimeoutError handler above. +for (name, command) in fmisimulators[1:]: + stat = {"sim": None, "diff": None, "phase": 5} + simulators[name] = stat + simFileOther = os.path.abspath("../files/%s_%s.sim" % (conf["fileName"], name)).replace('\\','/') + other = resultFile(name) + start = monotonic() + try: + simulateFmu(name, command, other, simFileOther) + stat["sim"] = monotonic()-start + stat["phase"] = 6 + verifyAgainstReference(other, artifactPrefix(name) + ".diff", stat) + except TimeoutError as e: + stat["sim"] = monotonic()-start + with open(errFile, 'a+') as fp: + fp.write("%s timed out simulating the FMU\n" % name) + writeResult() + +# quit omc_new: every verification needed it +omc_new = quit_omc(omc_new) writeResultAndExit(0) diff --git a/testresults.md b/testresults.md new file mode 100644 index 0000000..c1bfa22 --- /dev/null +++ b/testresults.md @@ -0,0 +1,77 @@ +# Test Results of Open-Source Modelica Libraries using OpenModelica + +## Test reports + +- **Overview with library test results across OMC versions**: + These reports shows how libraries with full or partial support level (see + [OMPackageManager, Library support levels](https://github.com/OpenModelica/OMPackageManager/blob/master/README.md#library-support-levels-in-openmodelica) + for more details) are handled by released versions of OpenModelica and by the + development version on the master branch. Regressions are marked in orange, + improvements in green. + + - [Default settings](https://libraries.openmodelica.org/branches/overview.html): + simulation with C runtime and default settings + - [daeMode](https://libraries.openmodelica.org/branches/overview-dae.html): + simulation with daeMode (compiler flag + [--daeMode](https://openmodelica.org/doc/OpenModelicaUsersGuide/latest/omchelptext.html#omcflag-daemode)) + - [C++](https://libraries.openmodelica.org/branches/overview-c++.html): + simulation with C++ runtime + - [FMI](https://libraries.openmodelica.org/branches/overview-fmi.html): + simulation with FMI and C runtime + - [FMI C++](https://libraries.openmodelica.org/branches/overview-c++.html): + simulation with FMI and C++ runtime + +- **Regression reports and history plots**: + Regression reports are periodically generated, using the latest development + version of OMC. Changes w.r.t. the previous report are shown. For each tested + library, .svg plots are also provided, showing the trends of the results of + each step over time. + + - [Default settings](https://libraries.openmodelica.org/branches/history/master/): + default omc settings, C runtime + - [daeMode](https://libraries.openmodelica.org/branches/history/daemode/): + simulation with --daeMode + - [C++](https://libraries.openmodelica.org/branches/history/cpp/): + simulation with C++ runtime + - [FMI](https://libraries.openmodelica.org/branches/history/master-fmi/): + simulation with FMI, C runtime + +## How to read the test reports + +The library testsuite job is run every night on the OSMC servers, testing +open-source Modelica libraries covered by the +[Package Manager](https://github.com/OpenModelica/OMPackageManager#readme) +with various versions of OpenModelica, including the development version on the +master branch. The simulations are run with the C-runtime, unless specified +differently. + +Within each library, all models with an experiment(StopTime) annotations are +run. For each tested model, the results of the following steps are reported: + +- _parsing_ +- _frontend_: flattening the model +- _backend_: structural analysis, index reduction, causalization, tearing, and + all kind of symbolic manipulations and optimization to solve the model + efficiently +- _simcode_: intermediate stap towards simulation code generation +- _templates_: generation of the actual C code +- _compilation_: compilation of the C code into a simulation executable +- _simulation_: the simulation is actually run to produce simulation results +- _verification_: if reference results file are available, they are compared + with the simulation results + +Clicking on the model name shows the log of phases from parsing to compilation. +Clicking on the (sim) link shows the log of the runtime simulation. + +## How to get your open-source library in the testsuite + +If you are actively developing an open-source Modelica library using a GIT +repository, you can easily get it included in the OpenModelica library +testsuite. Please open a request on the +[OpenModelica issue tracker](https://github.com/OpenModelica/OpenModelica/issues/new/choose). +Ideally, you should have two tests for it: one testing the latest official +release, which can be used to check if changes to OMC cause regressions to the +existing library, another one testing the development version, which also checks +regressions caused by changes to the library code. You can then bookmark the +page with the regression reports and the page with the test results obtained +with your favourite settings.