pax_global_header00006660000000000000000000000064124230470050014507gustar00rootroot0000000000000052 comment=e003b9828f76ba08d7534012a7433df0bd07ad0e asterisk-testsuite-0.0.0+svn.5781/000077500000000000000000000000001242304700500165655ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/COPYING000066400000000000000000000431021242304700500176200ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 19yy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) 19yy name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. asterisk-testsuite-0.0.0+svn.5781/Makefile000066400000000000000000000007611242304700500202310ustar00rootroot00000000000000# # Copyright (C) 2011, Digium, Inc. # Paul Belanger # # This program is free software, distributed under the terms of # the GNU General Public License Version 2. # all: make -C asttest all clean: _clean _clean: make -C asttest clean dist-clean: distclean distclean: _clean make -C asttest distclean rm -rf doc/api install: make -C asttest install uninstall: make -C asttest uninstall update: asttest: progdocs: (cat contrib/testsuite-doxygen) | doxygen - asterisk-testsuite-0.0.0+svn.5781/README.txt000066400000000000000000000572211242304700500202720ustar00rootroot00000000000000================================================================================ === === === Asterisk Test Suite === === === === http://www.asterisk.org/ === === Copyright (C) 2010 - 2012, Digium, Inc. === === === ================================================================================ -------------------------------------------------------------------------------- --- 0) Table of Contents -------------------------------------------------------------------------------- Using the Test Suite: 1) Introduction 2) Test Suite System Requirements 3) Running the Test Suite Writing Tests: 4) Test Anatomy 5) Test Configuration 6) Tests in Python 7) Tests in Lua -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- --- 1) Introduction -------------------------------------------------------------------------------- Over the years as the Asterisk code base has expanded, the need for more tools to control the quality of the code has increased. Luckily some of these tools have been implemented and the return on that investment has paid dividends immediately. There are four parts to code testing: 1) Testing with our eyes 2) Bottom-Up testing using unit tests within Asterisk 3) Top-Down testing using an external test suite 4) Tests running constantly using a continuous integration framework With the introduction of ReviewBoard (http://reviewboard.asterisk.org) code is now peer reviewed to a greater extent prior to being merged and the number of pre-commit bugs being found is tremendous. ReviewBoard satisfies the first part: Testing with our eyes. But where peer reviewing fails is in the ability to verify that regressions are not being introduced into the code. Whenever you solve a complex issue, the chances that a regression is introduced somewhere else is elevated. A way of minimizing those regressions is through automated testing. Automated testing improves the quality of code at any part of the development cycle and reduces the number of regressions being introduced. Whenever a part of the system is being worked on and bugs are being resolved, developers are encouraged to write tests in order to verify that the same issue does not creep back into the code, and that changes in other locations do not disrupt the expected results in that area. The next two directions satisfy the bottom-up testing and top-down testing methods: Automated testing for Asterisk is approached from two directions. The first is bottom-up unit testing. Those tests are implemented within Asterisk in the C programming language, using the Asterisk C APIs. These tests are enabled by turning on the TEST_FRAMEWORK compile time option in menuselect. The CLI commands related to the test framework all begin with "test". The second approach is top down using tests developed outside of Asterisk. This test suite is the collection of top-down functionality tests. The test suite is made up as a collection of scripts that test some portion of Asterisk functionality given a set of preconditions, and then provide a pass/fail result via a predefined method of doing so. The fourth part ties parts two and three together by making sure that whenever something is introduced that breaks one of the tests, that it gets resolved immediately and not at some point in the future through bug reporting. This is done with Bamboo. You can see the history and current status of the tests being run by visiting http://bamboo.asterisk.org. This document will focus on how you can setup the Asterisk Test Suite in order to run the same automated external tests on your own development system. You are also encouraged to write your own automated tests to verify parts of your own system remain in working order, and to contribute those tests back to the Asterisk project so they may be run in the automated testing framework. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- --- 2) Test Suite System Requirements -------------------------------------------------------------------------------- Required: - python >= 2.6.5 - python-yaml - git-core Note: Many commands below will install files into system directories; if you are executing these commands as an unprivileged user, you might need to use 'sudo' or similar. Optional (needed by specific tests): - bash - SIPp - Download the latest unstable release - http://sipp.sourceforge.net/snapshots/ - Compile the version with pcap and OpenSSL support using: $ make pcapplay_ossl - Install sipp into a directory in the execution path: $ cp sipp /usr/local/bin - asttest - include with the test suite - Install from the asttest directory $ cd asttest $ make $ make install - Python modules - starpy - Install starpy from the addons directory $ cd addons $ make update $ make install - python-twisted - pjsua - Download and build pjproject 1.x from source - http://www.pjsip.org/download.htm - On an x86-32 machine, run the configure script as follows: $ ./configure - On an x86-64 machine, run the configure script as follows: $ ./configure CFLAGS=-fPIC - There are tests in the testsuite that require IPv6 support in pjsua, so create a pjlib/include/pj/config_site.h file and add the line #define PJ_HAS_IPV6 1 - Build $ make dep && make - On an x86-32 machine, copy the 'pjsua-x86-unknown-linux-gnu' executable found in the pjsip-apps/bin/ directory to a directory located in the execution path, and name it 'pjsua'. $ cp pjsip-apps/bin/pjsua-x86-unknown-linux-gnu /usr/local/bin/pjsua - On an x86-64 machine, copy the 'pjsua-x86_64-unknown-linux-gnu' executable found in the pjsip-apps/bin/ directory to a directory located in the execution path, and name it 'pjsua'. $ cp pjsip-apps/bin/pjsua-x86_64-unknown-linux-gnu /usr/local/bin/pjsua - pjsua python bindings - Go to pjsip-apps/src/python directory - Run 'sudo python ./setup.py install' or just 'sudo make install' - libpcap and yappcap - Install the libpcap development library package for your system (libpcap-dev for Debian-based distros, pcap-devel for Red Hat) - Install cython - Download yappcap from: https://github.com/otherwiseguy/yappcap/tarball/master - tar -xvzf otherwiseguy-yappcap*.tar.gz - cd otherwiseguy-yappcap* - make && sudo make install - Note that if you install these packages, you'll need to execute tests in the testsuite using an account with privileges to do raw packet captures ('root', of course, but other accounts may work on your system). -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- --- 3) Running the Test Suite -------------------------------------------------------------------------------- Get the Asterisk source tree you want to test: $ svn co http://svn.digium.com/svn/asterisk/trunk asterisk-trunk $ cd asterisk-trunk Build and install it. $ ./configure && make $ make install Check out the test suite: $ svn co http://svn.digium.com/svn/testsuite/asterisk/trunk testsuite $ cd testsuite List the tests: $ ./runtests.py -l ****************************************** *** Listing the tests will also tell *** *** you which dependencies are missing *** ****************************************** Run the tests: # ./runtests.py For more syntax information: $ ./runtests.py --help As an alternative to the above, you can use run-local: Get the Asterisk source tree you want to test: $ svn co http://svn.digium.com/svn/asterisk/trunk asterisk-trunk $ cd asterisk-trunk Optionally configure and make it: $ ./configure && make Check out the test suite: $ svn co http://svn.digium.com/svn/testsuite/asterisk/trunk testsuite $ cd testsuite Setup the test environment: $ ./run-local setup Run tests: $ ./run-local run # Add here any of runtests.py's parameters. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- --- 4) Test Anatomy -------------------------------------------------------------------------------- a) File layout Adding a test to the test suite is very easy to do. Every test lives within its own directory within the tests directory. Within that directory, there must be an executable called "run-test". The test directory may contain anything else that the test needs to do its job. A test configuration file may also exist that includes parameters that will be read by the top level test script. /tests /mynewtest -> run-test -> test-config.yaml ... /configs -> asterisk.options.conf.inc -> logger.conf -> logger.general.conf.inc ... To have a test included in the test suite, it must be added to the "tests.yaml" file that lives in the tests directory. This configuration file determines the order that the tests are considered for execution by the top level test suite application. The purpose of the 'configs' directory is to define global settings for Asterisk. Tests will inherit these settings every time the testsuite creates sandbox instances of Asterisk. Additionally, tests have the ability to override these setting, however it is NOT recommended they do so. If you wanted to add a setting to logger.conf [logfiles], you could create a 'logger.logfiles.conf.inc' file for your test and the global Asterisk logger.conf will automatically include it. The filename convention is ..conf.inc. Again, settings in 'asterisk.options.conf.inc' would be included in asterisk.conf [options] category. b) Preconditions When the "run-test" application is executed, it can assume that the system has a fresh install of Asterisk with files installed into their default locations. This includes a fresh set of sample configuration files in the /etc/asterisk directory. For increased portability the shebang (#!) for "run-test" MUST begin with "#!/usr/bin/env". For exmaple: a test written in Python would have "#!/usr/bin/env python" for the shebang. c) Test configuration files All configuration files will now be stored in the 'configs/ast%d' directory, depending on how many Asterisk instances your test uses, you create additional 'ast%d' sub folders. If you only use 1 Asterisk instance, all files will be copied to 'configs/ast1'. For example, we can see the 'basic-call' test below will use 2 Asterisk instances. However, assume both instances have the same extensions.conf file, instead duplicating data by copying it into 'ast1' and 'ast2', shared configuration files SHOULD be copied into the root 'configs' directory. basic-call/ configs/ ast1/ sip.conf ... ast2/ sip.conf ... extensions.conf run-test Since each Asterisk instance required difference SIP settings, each 'ast%d' folder will have a different sip.conf file. d) Test Execution The "run-test" executable will be run by a top level application in the test suite called "runtests.py". When "run-test" is executed, it will be provided a standard set of arguments which are defined here: -v # Will always be provided e) Logging, Pass/Fail Reporting All test output, including failure details, should be send to STDOUT. If the test has failed, the "run-test" executable should exit with a non-zero return code. A return code of zero is considered a success. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- --- 5) Test Configuration -------------------------------------------------------------------------------- Test configuration lives in a file called "test-config.yaml". The format for the configuration file is YAML. More information on YAML can be found at http://www.yaml.org/. # # Example test configuration file - test-config.yaml # # All elements are required unless explicitly noted as OPTIONAL. If marked # GLOBAL, those elements are only processed if they exist in the top level # test-config.yaml file, which applies global options across the test. # # The global settings section, which defines which test configuration to execute # and other non-test specific information global-settings: # GLOBAL # The active test configuration. The value must match a subsequent key # in this file, which defines the global settings to apply to the test execution # run. test-configuration: config-pessimistic # GLOBAL # The following sequence defines for any test configuration the available pre- # and post-test conditions. The 'name' field specifies how the test configurations # refer to the pre- and post-test conditions in order to activate them. condition-definitions: # GLOBAL - name: 'threads' # A pre-test condition, which specifies that the object will be evaluated # prior to test execution pre: # The fully qualified Python typename of the object to create using # introspection typename: 'asterisk.ThreadTestCondition.ThreadPreTestCondition' post: typename: 'asterisk.ThreadTestCondition.ThreadPostTestCondition' # The fully qualified Python typename of the object to pass to the evaluate # function of this object related-type: 'asterisk.ThreadTestCondition.ThreadPreTestCondition' - name: 'sip-dialogs' pre: typename: 'asterisk.SipDialogTestCondition.SipDialogPreTestCondition' post: typename: 'asterisk.SipDialogTestCondition.SipDialogPostTestCondition' - name: 'locks' pre: typename: 'asterisk.LockTestCondition.LockTestCondition' post: typename: 'asterisk.LockTestCondition.LockTestCondition' - name: 'file-descriptors' pre: typename: 'asterisk.FdTestCondition.FdPreTestCondition' post: typename: 'asterisk.FdTestCondition.FdPostTestCondition' related-type: 'asterisk.FdTestCondition.FdPreTestCondition' - name: 'channels' pre: typename: 'asterisk.ChannelTestCondition.ChannelTestCondition' post: typename: 'asterisk.ChannelTestCondition.ChannelTestCondition' # A global test definition. This name can be anything, but must be referenced # by the global-settings.test-configuration key. config-pessimistic: # GLOBAL # A list of tests to explicitly exclude from execution. This overrides the # test listsing in the tests.yaml files. exclude-tests: # GLOBAL # The name of a test to exclude. Name matching is done using the Python # in operator. - 'authenticate_invalid_password' properties: # The test conditions to apply to all tests. See specific configuration # information for the test conditions in the individual test configuration # section below. testconditions: - name: 'threads' - name: 'sip-dialogs' - name: 'locks' - name: 'file-descriptors' - name: 'channels' # The testinfo section contains information that describes the purpose of an # individual test testinfo: skip : 'Brief reason for skipping test' # OPTIONAL summary : 'Put a short one liner summary of the test here' issues : | # List of issue numbers associated with this test # OPTIONAL - mantis : '12345' - mantis : '10101' description : | 'Put a more verbose description of the test here. This may span multiple lines.' # The properties section contains information about requirements and # dependencies for this test. properties: minversion : '1.8.0.0' # minimum Asterisk version compatible with this test buildoption : 'TEST_FRAMEWORK' # OPTIONAL - Asterisk compilation flag maxversion : '10.5.1' # OPTIONAL features: # List features the Asterisk version under test must support for this test # to execute. All features must be satisfied for the test to run. - 'digiumphones' - 'cert' dependencies : | # OPTIONAL # List dependencies that must be met for this test to run # # Checking for an 'app' dependency behaves like the 'which' command - app : 'bash' - app : 'sipp' # A 'python' dependency is a python module. An attempt will be made to # import a module by this name to determine whether the dependency is # met. - python : 'yaml' # A 'module' dependency is an Asterisk module that must be loaded by # Asterisk in order for this test to execute. If the module is not loaded, # the test will not execute. - module : 'app_dial' # 'custom' dependency can be anything. Checking for this dependency is # done by calling a corresponding method in the Dependency class in # runtests.py. For example, if the dependency is 'ipv6', then the # depend_ipv6() method is called to determine if the dependency is met. - custom : 'ipv6' - custom : 'fax' testconditions: # OPTIONAL # # List of overrides for pre-test and post-test conditions. If a condition is # defined for a test, the configuration of that condition in the test overrides # the setting defined in the global test configuration file. # - # Check for thread usage in Asterisk. Any threads present in Asterisk after test # execution - and any threads that were detected prior to test execution # that are no longer present - will be flagged as a test error. name: 'threads' # # Disable execution of this condition. This setting applies to any defined condition. # Any other value but 'False' will result in the condition being executed. enabled: 'False' # # Execute the condition, but expect the condition to fail expectedResult: 'Fail' # # The thread test condition allows for certain detected threads to be # ignored. This is a list of the thread names, as reported by the CLI # command 'core show threads' ignoredThreads: - 'netconsole' - 'pbx_thread' # - # Check for SIP dialog usage. This looks for any SIP dialogs present # in the system before and after a run; if any are present and are not # scheduled for destruction, an error is raised. name: 'sip-dialogs' # # In addition to checking for scheduled destruction, a test can request that # certain entries should appear in the SIP history. If the entries do not # appear, an error is raised. sipHistoryRequirements: - 'NewChan' - 'Hangup' # - # Check for held locks in Asterisk after test execution. A lock is determined to # be in potential error if threads are detected holding mutexes and waiting on # other threads that are also holding mutexes. name: 'locks' # - # Check for active channels in Asterisk. If active channels are detected, flag # an error name: 'channels' # # The number of allowed active channels that can exist when the condition is checked. # If the number of channels detected is greater than this value, an error is raised. # By default, this value is 0. allowedchannels: 1 # - # Check for active file descriptors in Asterisk. File descriptors detected before # test execution are tracked throughout the test; if any additional file descriptors # after test execution are detected, the test condition fails. name: 'file-descriptors' tags: # OPTIONAL # # List of tags used to select a subset of tests to run. A test must have all tags to run. # - core # This test is part of the core support level. - voicemail # This test involves voicemail functionality. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- --- 6) Tests in Python -------------------------------------------------------------------------------- There are some python modules included in lib/python/ which are intended to help with writing tests in python. Python code in the testsuite should be formatted according to PEP8: http://www.python.org/dev/peps/pep-0008/. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- --- 7) Tests in Lua -------------------------------------------------------------------------------- The asttest framework included in the asttest directory provides a lot of functionality to make it easy to write Asterisk tests in Lua. Take a look at the asttest README as well as some of the existing Lua tests for more information. -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- ================================================================================ ================================================================================ asterisk-testsuite-0.0.0+svn.5781/TODO.txt000066400000000000000000000025031242304700500200730ustar00rootroot00000000000000================================================================================ === === === Asterisk Test Suite === === === === http://www.asterisk.org/ === === Copyright (C) 2010 - 2011, Digium, Inc. === === === ================================================================================ TODO List: * Make it so the test suite can be executed as non-root. * Make the test suite independent of a system installation of Asterisk. Make it so running the test suite does not disturb the system installation of Asterisk (if there is one), and still work just fine if one is not there. * Add knowledge of ABE versions to the lib/python/asterisk/version.py module. * Covert existing tests to use new TestCase python library. * Replace cli_originate() with ami.originate() then remove cli_originate() function. ================================================================================ ================================================================================ asterisk-testsuite-0.0.0+svn.5781/addons/000077500000000000000000000000001242304700500200355ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/addons/Makefile000066400000000000000000000016401242304700500214760ustar00rootroot00000000000000# # Copyright (C) 2010, Digium, Inc. # Paul Belanger # # This program is free software, distributed under the terms of # the GNU General Public License Version 2. # all: @if [ ! -d starpy ]; then \ echo "starpy/ does not exist. Run \`make update\` to checkout via git."; \ else \ echo "Run \`make install\`."; \ fi clean: _clean _clean: dist-clean: distclean distclean: _clean rm -rf starpy/install.record install: (cd starpy; python setup.py install --prefix=~/.local --record install.record) uninstall: rm -rf $$(cat starpy/install.record) update: @if [ -d starpy ]; then \ cd starpy; \ if [ `git config --get remote.origin.url` = git://github.com/asterisk-org/starpy.git ]; then \ git remote set-url origin git://github.com/asterisk/starpy.git git://github.com/asterisk-org/starpy.git; \ fi; \ git pull; \ else \ git clone git://github.com/asterisk/starpy.git; \ fi asterisk-testsuite-0.0.0+svn.5781/asttest/000077500000000000000000000000001242304700500202545ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/INSTALL000066400000000000000000000020151242304700500213030ustar00rootroot00000000000000Building and installing asttest =============================== Asttest can be built and installed by running 'make install'. Dependencies ============ Asttest depends on the following libraries and tools. pkg-config lua 5.1 (liblua5.1 and luac) Building asterisk for use with asttest ====================================== Asterisk can be built for use with asttest using the following command. ASTSRCDIR=/usr/src/asterisk ASTDSTDIR=/tmp/asterisk make asterisk Where ASTSRCDIR is the path to your asterisk source and ASTDSTDIR is the path to the directory where asterisk should be installed. Asterisk will be built then installed in the directory specified. The asterisk installation will be entirely self contained in ASTDSTDIR. This directory can be passed to asttest using the -a flag to tell it to use the new install for testing. asttest -a /tmp/asterisk my-test-dir To simply use a pre-existing installation of asterisk on a system, asttest can be run as follows. asttest -a / my-test-dir vim: set fo=tqwal: asterisk-testsuite-0.0.0+svn.5781/asttest/Makefile000066400000000000000000000132511242304700500217160ustar00rootroot00000000000000# # Asterisk -- A telephony toolkit for Linux. # # Makefile for asttest # # Copyright (C) 2005-2008, Digium, Inc. # # Matthew Nicholson # # This program is free software, distributed under the terms of # the GNU General Public License # # even though we could use '-include makeopts' here, use a wildcard # lookup anyway, so that make won't try to build makeopts if it doesn't # exist (other rules will force it to be built if needed) ifneq ($(wildcard makeopts),) include makeopts endif .PHONY: clean dist-clean distclean test check asterisk INSTALL?=install DESTDIR?= PREFIX?=/usr/local EXEC_PREFIX?=$(PREFIX) BINDIR?=$(EXEC_PREFIX)/bin ASTSRCDIR ?= ../ ASTDSTDIR ?= $(PWD)/asterisk LUAFILESYSTEM=luafilesystem-1.4.2 LUASOCKET=luasocket-2.0.2 LUAPOSIX=luaposix-5.1.4 LUAFILESYSTEM_OBJS=lib/lua/$(LUAFILESYSTEM)/src/lfs.o LUASOCKET_OBJS=lib/lua/$(LUASOCKET)/src/luasocket.a LUAPOSIX_OBJS=lib/lua/$(LUAPOSIX)/lposix.o LUAFILESYSTEM_HEADER=lib/lua/$(LUAFILESYSTEM)/src/lfs.h LUASOCKET_HEADER=lib/lua/$(LUASOCKET)/src/luasocket.h LUAPOSIX_HEADER=lib/lua/$(LUAPOSIX)/lposix.h LUAPOSIX_LIBS=-lcrypt # Basic set of sources and flags/libraries/includes OBJS:=asttest.o lib/lua.o lib/testsuite.o lib/testutils.o CFLAGS:=-g -D_GNU_SOURCE -Wall `pkg-config --cflags lua5.1 2> /dev/null || pkg-config --cflags lua 2> /dev/null` -Iinclude L_MODULES:=$(LUAFILESYSTEM_OBJS) $(LUASOCKET_OBJS) $(LUAPOSIX_OBJS) L_LIBS:=$(LUAPOSIX_LIBS) L_OBJS:=lib/lua/testlib.o lib/lua/astlib.o lib/lua/proclib.o T_LIBS:=`pkg-config --libs lua5.1 2> /dev/null || pkg-config --libs lua 2> /dev/null || echo -llua -lm -ldl` all: asttest #$(OBJS) $(C_OBJS): autoconfig.h menuselect.h # #makeopts autoconfig.h: autoconfig.h.in makeopts.in # @./configure $(CONFIGURE_SILENT) CC= LD= AR= CFLAGS= # #ifdef C_OBJS #menuselect_curses.o: CFLAGS+=$(C_INCLUDE) #cmenuselect: $(OBJS) $(C_OBJS) # $(CC) -o $@ $^ $(C_LIBS) #else #cmenuselect: #endif # #ifdef G_OBJS #menuselect_gtk.o: CFLAGS+=$(G_INCLUDE) #gmenuselect: $(OBJS) $(G_OBJS) # $(CC) -o $@ $^ $(G_LIBS) #else #gmenuselect: #endif # #ifdef N_OBJS #menuselect_newt.o: CFLAGS+=$(N_INCLUDE) #nmenuselect: $(OBJS) $(N_OBJS) # $(CC) -o $@ $^ $(N_LIBS) #else #nmenuselect: #endif lib/lua/%_lua.h : lib/lua/%.lua tools/mkstring luac -o $(basename $<).luac $< ./tools/mkstring -n $(notdir $(basename $<))_lua -o $@ $(basename $<).luac lib/%.o: lib/%.c include/asttest/%.h # this line does not seem to work #lib/lua/%.o: lib/lua/%.c lib/lua/%_lua.h include/asttest/lua/%.h asterisk: cd $(ASTSRCDIR) && ./configure --enable-dev-mode \ --prefix=$(ASTDSTDIR)/usr \ --sysconfdir=$(ASTDSTDIR)/etc \ --localstatedir=$(ASTDSTDIR)/var $(MAKE) -C $(ASTSRCDIR) install $(MAKE) -C $(ASTSRCDIR) samples lib/testsuite.o: lib/testsuite.c include/asttest/testsuite.h include/asttest/asttest.h lib/testutils.o: lib/testutils.c include/asttest/testutils.h include/asttest/asttest.h include/asttest/testsuite.h include/asttest/lua.h lib/lua.o: lib/lua.c $(L_MODULES) include/asttest/lua.h include/asttest/testsuite.h include/asttest/lua/*.h $(CC) $(CFLAGS) -c -o $@ \ -DLUAFILESYSTEM_HEADER=\"../$(LUAFILESYSTEM_HEADER)\" \ -DLUASOCKET_HEADER=\"../$(LUASOCKET_HEADER)\" \ -DLUAPOSIX_HEADER=\"../$(LUAPOSIX_HEADER)\" \ $< lib/lua/testlib.o: lib/lua/testlib.c lib/lua/testlib_lua.h include/asttest/lua/testlib.h lib/lua/astlib.o: lib/lua/astlib.c lib/lua/astlib_lua.h include/asttest/lua/astlib.h lib/lua/proclib.o: lib/lua/proclib.c lib/lua/proclib_lua.h include/asttest/lua/proclib.h $(LUAFILESYSTEM_OBJS): lib/lua/$(LUAFILESYSTEM) lib/lua/lfs-patched.stamp $(MAKE) -C lib/lua/$(LUAFILESYSTEM) lib/lua/lfs-patched.stamp lib/lua/$(LUAFILESYSTEM): lib/lua/$(LUAFILESYSTEM).tar.gz tools/lfs.diff rm -rf lib/lua/$(LUAFILESYSTEM) tar -C lib/lua -zxf lib/lua/$(LUAFILESYSTEM).tar.gz patch -p1 -d lib/lua/$(LUAFILESYSTEM) < tools/lfs.diff touch lib/lua/lfs-patched.stamp $(LUASOCKET_OBJS): lib/lua/$(LUASOCKET) lib/lua/luasocket-patched.stamp $(MAKE) -C lib/lua/$(LUASOCKET) lib/lua/luasocket-patched.stamp lib/lua/$(LUASOCKET): lib/lua/$(LUASOCKET).tar.gz tools/luasocket.diff rm -rf lib/lua/$(LUASOCKET) tar -C lib/lua -zxf lib/lua/$(LUASOCKET).tar.gz patch -p1 -d lib/lua/$(LUASOCKET) < tools/luasocket.diff touch lib/lua/luasocket-patched.stamp $(LUAPOSIX_OBJS): lib/lua/$(LUAPOSIX) lib/lua/luaposix-patched.stamp $(MAKE) -C lib/lua/$(LUAPOSIX) lib/lua/luaposix-patched.stamp lib/lua/$(LUAPOSIX): lib/lua/$(LUAPOSIX).tar.gz tools/luaposix.diff rm -rf lib/lua/$(LUAPOSIX) tar -C lib/lua -zxf lib/lua/$(LUAPOSIX).tar.gz patch -p1 -d lib/lua/$(LUAPOSIX) < tools/luaposix.diff touch lib/lua/luaposix-patched.stamp asttest: asttest.c $(OBJS) $(T_OBJS) $(L_OBJS) include/asttest/asttest.h $(CC) -o $@ $(OBJS) $(L_OBJS) $(T_LIBS) $(L_MODULES) $(L_LIBS) tools/mkstring: tools/mkstring.c $(CC) -D_GNU_SOURCE -Wall -o $@ $^ install: asttest install -d $(DESTDIR)$(BINDIR) install -m 755 asttest $(DESTDIR)$(BINDIR) uninstall: rm -f $(DESTDIR)$(BINDIR)/asttest check: test tests: test test: asttest LUA_PATH=$(PWD)/../lib/lua/?.lua\;\; \ LUA_CPATH=$(PWD)/../lib/lua/\?.so\;\; \ $(GDB) ./asttest self-tests clean: rm -f asttest $(OBJS) $(M_OBJS) $(C_OBJS) $(G_OBJS) $(N_OBJS) $(L_OBJS) rm -f lib/lua/*_lua.h rm -f lib/lua/*.luac rm -f tools/mkstring -$(MAKE) -C lib/lua/$(LUAFILESYSTEM) clean -$(MAKE) -C lib/lua/$(LUASOCKET) clean -$(MAKE) -C lib/lua/$(LUAPOSIX) clean dist-clean: distclean distclean: clean rm -rf $(ASTDSTDIR) rm -f autoconfig.h config.status config.log makeopts rm -rf autom4te.cache rm -rf lib/lua/$(LUAFILESYSTEM) rm -rf lib/lua/$(LUASOCKET) rm -rf lib/lua/$(LUAPOSIX) rm -f lib/lua/lfs-patched.stamp rm -f lib/lua/luasocket-patched.stamp rm -f lib/lua/luaposix-patched.stamp asterisk-testsuite-0.0.0+svn.5781/asttest/README000066400000000000000000000042511242304700500211360ustar00rootroot00000000000000Asttest Readme ============== Asttest is a framework for doing automated testing of asterisk. It can be used to spawn asterisk and other processes, interact with the manager interface, and other related testing tasks. Writing Tests ============= The structure of a test will be different depending on what is being tested. All tests require a test.lua file in the given test directory. The directory layout for tests is as follows: main_test_dir/ test1/ test.lua test2/ test.lua test3/ test.lua The test.lua file will be executed by asttest when the test is run. The test.lua file is responsible for coordinating execution of the test. This file may generate config files, start asterisk, interact with asterisk via the manager interface, parse log files, etc. Test.lua is also responsible for notifiying asttest of the test result. When running in single test mode (the -s flag, see below) the directory structure is as follows: test1/ test.lua When a test is run, the working directory will be changed to the test directory and all paths in the test will be intrepreted relative to that directory. Running Tests ============= Asttest can be run in two modes. It can be used to execute a directory full of tests, or it can execute a single test. Documentation for the various options asttest accepts can be found by passing the -h flag to asttest. To execute a directory full of tests, pass asttest the path to the directory. asttest my-test-dir/ To execute a single test (useful for interfacing with run-tests.py), pass asstest the -s flag specifiying the directory the desired test is in. asttest -s my-test-dir/my-test When executing in single test mode, asttest will send all output to stdout and will return non-zero on error. Using asttest with run-tests.py =============================== To use asttest with run-tests.py, a script similar to the following can be used. #!/bin/bash -e . lib/sh/lua.sh asttest -a / -s `dirname $0` $@ The -a option indicates the location of the asterisk installation to use for the test and the -s option turns on single test mode and indicates the directory the test is located in. vim: set fo=tqwal: asterisk-testsuite-0.0.0+svn.5781/asttest/asttest.c000066400000000000000000000103001242304700500221010ustar00rootroot00000000000000/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 1999 - 2008, Digium, Inc. * * Matthew Nicholson * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ #include #include #include #include "asttest/asttest.h" #include "asttest/testutils.h" const char *default_log_filename = "asttest.log"; void usage(const char *prog_name) { fprintf(stderr, "Usage:\n" " %s [-wh] [-v ] [-l ] [-a ] [test_dir...]\n" " %s [-wh] [-v ] [-l ] [-a ] -s \n" "\n" "Options:\n" " -l Specify the name of the log file. One log file will be\n" " created for each test directory in that test directory\n" " using the given name. The default filename is\n" " asttest.log.\n" " -a Specify the directory asterisk has been installed in.\n" " This option is roughly equivalent to the value of\n" " --prefix at configure time. The actually asterisk binary\n" " is expected to be located at /sbin/asterisk.\n" " The default is 'asterisk'.\n" " -w warn if tests were skipped because of errors. This\n" " option will cause a warning to print instead of an\n" " error being generated if any tests fail because of\n" " errors. This is ignored in single test mode.\n" " -h print this help message\n" " -s Run in single test mode. The given directory should contain a\n" " test.lua test script. Single test mode will also cause test\n" " output to be sent to stdout instead of a log file and will\n" " cause the program to exit with a non zero return code in if\n" " the test fails.\n" " -v Specify the version of asterisk we are testing against. If\n" " not specified, the version.h file from the specified asterisk\n" " path will be usedi to determine the version number.\n" "\n" , prog_name, prog_name); } /* * \brief Parse command line options. * @param argc the argument count * @param argv an array of strings * @param opts the struct where our options will be stored * * @note If this function returns 0 the remaining options should be test * directories. * * @retval 1 -h option * @retval -1 error * @return 0 success */ int parse_cmdline(int argc, char *argv[], struct asttest_opts *opts) { char c; memset(opts, 0, sizeof(struct asttest_opts)); /* set some default options */ opts->warn_on_error = 0; opts->log_filename = default_log_filename; opts->asterisk_path = "asterisk"; /* parse options */ while ((c = getopt(argc, argv, "l:a:s:v:wh")) != -1) { switch (c) { case 'l': opts->log_filename = optarg; break; case 'w': opts->warn_on_error = 1; break; case 'a': opts->asterisk_path = optarg; break; case 's': opts->single_test_mode = optarg; break; case 'v': opts->asterisk_version = optarg; break; case 'h': return 1; case '?': return -1; break; } } return 0; } int main(int argc, char *argv[]) { int res = 0; int i; struct asttest_opts opts; if ((res = parse_cmdline(argc, argv, &opts))) { usage(argv[0]); if (res == 1) return 0; else return 1; } if (opts.single_test_mode) { return process_single_test(&opts); } else { if (optind == argc) { fprintf(stderr, "%s: missing arguments -- specify at least one test directory\n", argv[0]); usage(argv[0]); return 1; } for (i = optind; i < argc; i++) { if (process_test_dir(argv[i], &opts)) { fprintf(stderr, "test suite failed, exiting...\n"); return 1; } } } return 0; } asterisk-testsuite-0.0.0+svn.5781/asttest/include/000077500000000000000000000000001242304700500216775ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/include/asttest/000077500000000000000000000000001242304700500233665ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/include/asttest/asttest.h000066400000000000000000000015611242304700500252310ustar00rootroot00000000000000/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 1999 - 2008, Digium, Inc. * * Matthew Nicholson * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ #ifndef ASTTEST_ASTTEST_H #define ASTTEST_ASTTEST_H #include struct asttest_opts { unsigned int warn_on_error:1; const char *log_filename; const char *asterisk_path; const char *asterisk_version; const char *single_test_mode; }; extern const char *default_log_filename; #endif asterisk-testsuite-0.0.0+svn.5781/asttest/include/asttest/lua.h000066400000000000000000000014221242304700500243170ustar00rootroot00000000000000/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 1999 - 2008, Digium, Inc. * * Matthew Nicholson * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ #ifndef ASTTEST_LUA_H #define ASTTEST_LUA_H #include #include "asttest/testsuite.h" #include "asttest/lua/testlib.h" lua_State *get_lua_state(struct testsuite *ts, const char *test_name); #endif asterisk-testsuite-0.0.0+svn.5781/asttest/include/asttest/lua/000077500000000000000000000000001242304700500241475ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/include/asttest/lua/astlib.h000066400000000000000000000012711242304700500255770ustar00rootroot00000000000000/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 1999 - 2008, Digium, Inc. * * Matthew Nicholson * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ #ifndef ASTTEST_LUA_ASTLIB_H #define ASTTEST_LUA_ASTLIB_H #include int luaopen_astlib(lua_State *L); #endif asterisk-testsuite-0.0.0+svn.5781/asttest/include/asttest/lua/proclib.h000066400000000000000000000012741242304700500257560ustar00rootroot00000000000000/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 1999 - 2008, Digium, Inc. * * Matthew Nicholson * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ #ifndef ASTTEST_LUA_PROCLIB_H #define ASTTEST_LUA_PROCLIB_H #include int luaopen_proclib(lua_State *L); #endif asterisk-testsuite-0.0.0+svn.5781/asttest/include/asttest/lua/testlib.h000066400000000000000000000015611242304700500257710ustar00rootroot00000000000000/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 1999 - 2008, Digium, Inc. * * Matthew Nicholson * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ #ifndef ASTTEST_LUA_TESTLIB_H #define ASTTEST_LUA_TESTLIB_H #include int testlib_expected_fail(lua_State *L); int testlib_atexit(lua_State *L, int result_index); int testlib_preprocess_result(lua_State *L); int testlib_default_result(lua_State *L); int luaopen_testlib(lua_State *L); #endif asterisk-testsuite-0.0.0+svn.5781/asttest/include/asttest/testsuite.h000066400000000000000000000035331242304700500255740ustar00rootroot00000000000000/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 1999 - 2008, Digium, Inc. * * Matthew Nicholson * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ #ifndef ASTTEST_TESTSUITE_H #define ASTTEST_TESTSUITE_H #include "asttest/asttest.h" struct testsuite { unsigned int pass; unsigned int fail; unsigned int xfail; unsigned int xpass; unsigned int skip; unsigned int error; unsigned int total; FILE *log; char asterisk_path[PATH_MAX]; const char *asterisk_version; unsigned int single_test_mode:1; }; enum ts_result { TS_PASS, TS_FAIL, TS_XFAIL, TS_XPASS, TS_SKIP, TS_ERROR, }; int ts_init(struct testsuite *ts, const char *path, struct asttest_opts *opts); int ts_init_single(struct testsuite *ts, struct asttest_opts *opts); void ts_cleanup(struct testsuite *ts); void ts_print(struct testsuite *ts); int ts_log_va(struct testsuite *ts, const char *test_name, const char *fmt, va_list ap); int __attribute__((format(printf, 3, 4))) ts_log(struct testsuite *ts, const char *test_name, const char *fmt, ...); enum ts_result ts_pass(struct testsuite *ts, const char *test_name); enum ts_result ts_fail(struct testsuite *ts, const char *test_name); enum ts_result ts_xpass(struct testsuite *ts, const char *test_name); enum ts_result ts_xfail(struct testsuite *ts, const char *test_name); enum ts_result ts_skip(struct testsuite *ts, const char *test_name); enum ts_result ts_error(struct testsuite *ts, const char *test_name); #endif asterisk-testsuite-0.0.0+svn.5781/asttest/include/asttest/testutils.h000066400000000000000000000014301242304700500255750ustar00rootroot00000000000000/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 1999 - 2008, Digium, Inc. * * Matthew Nicholson * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ #ifndef ASTTEST_TESTUTILS_H #define ASTTEST_TESTUTILS_H #include "asttest/asttest.h" int process_single_test(struct asttest_opts *opts); int process_test_dir(const char *path, struct asttest_opts *opts); #endif asterisk-testsuite-0.0.0+svn.5781/asttest/lib/000077500000000000000000000000001242304700500210225ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/lib/lua.c000066400000000000000000000041401242304700500217460ustar00rootroot00000000000000/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 1999 - 2008, Digium, Inc. * * Matthew Nicholson * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ #include #include #include #include "asttest/lua.h" #include "asttest/testsuite.h" #include "asttest/lua/astlib.h" #include "asttest/lua/testlib.h" #include "asttest/lua/proclib.h" #include LUAFILESYSTEM_HEADER #include LUASOCKET_HEADER #include LUAPOSIX_HEADER lua_State *get_lua_state(struct testsuite *ts, const char *test_name) { lua_State *L = luaL_newstate(); if (!L) { goto e_return; } luaL_openlibs(L); /* luad LuaFileSystem */ lua_pushcfunction(L, luaopen_lfs); if (lua_pcall(L, 0, 0, 0)) { goto e_print_error; } /* load LuaSocket */ lua_pushcfunction(L, luaopen_socket_core); if (lua_pcall(L, 0, 0, 0)) { goto e_print_error; } /* load LuaPosix */ lua_pushcfunction(L, luaopen_posix); if (lua_pcall(L, 0, 0, 0)) { goto e_print_error; } /* load the test lib */ lua_pushcfunction(L, luaopen_testlib); lua_pushlightuserdata(L, ts); lua_pushstring(L, test_name); if (lua_pcall(L, 2, 0, 0)) { goto e_print_error; } /* load the proc lib */ lua_pushcfunction(L, luaopen_proclib); if (lua_pcall(L, 0, 0, 0)) { goto e_print_error; } /* load the asterisk lib */ lua_pushcfunction(L, luaopen_astlib); lua_pushstring(L, ts->asterisk_path); if (ts->asterisk_version) lua_pushstring(L, ts->asterisk_version); else lua_pushnil(L); if (lua_pcall(L, 2, 0, 0)) { goto e_print_error; } return L; e_print_error: /* we expect an error string on the top of the stack */ ts_log(ts, test_name, "%s\n", lua_tostring(L, -1)); /*e_close_lua:*/ lua_close(L); e_return: return NULL; } asterisk-testsuite-0.0.0+svn.5781/asttest/lib/lua/000077500000000000000000000000001242304700500216035ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/lib/lua/astlib.c000066400000000000000000000342221242304700500232300ustar00rootroot00000000000000/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 1999 - 2008, Digium, Inc. * * Matthew Nicholson * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ #include #include #include #include "astlib_lua.h" #include #include #include #include #include #include #include #include #include #include /*! * \brief Make the parent directories of a given path. * \param pathname the path to create * \param mode the mode of the created directories * * Create the parent directories of a given path if they do not exist. If the * given string does not end in '/' then the last path component will be * treated as a file name and ignored. * * \note In the case of an error, errno should be set. * \retval 0 success * \retval -1 error */ static int mkdir_p(const char *pathname, mode_t mode) { char buf[PATH_MAX + 1]; const char *c = pathname; while ((c = strchr(c, '/'))) { c++; if (c - pathname > PATH_MAX) { errno = ENAMETOOLONG; goto e_return; } strncpy(buf, pathname, c - pathname); buf[c - pathname] = '\0'; if (mkdir(buf, mode) && errno != EEXIST) { goto e_return; } } return 0; e_return: return -1; } /*! * \brief Symlink a file. * \param L the lua state to use * \param src the source file * \param dst the destination file * * \retval 0 success * \retval -1 error */ static int symlink_file(lua_State *L, const char *src, const char *dst) { if (symlink(src, dst)) { lua_pushstring(L, "error symlink '"); lua_pushstring(L, dst); lua_pushstring(L, "': "); lua_pushstring(L, strerror(errno)); lua_concat(L, 4); goto e_return; } return 0; e_return: return -1; } /*! * \brief Recursively symlink and copy a directory. * \param L the lua state to use * \param src the source directory * \param dst the destination directory * * This function recursively creates symlinks to files in src in the dst * directory. It does not symlink directories and instead makes new * directories in dst matching the corisponding dir in src. * * \note On error an error message is pushed onto the given lua stack. * * \retval 0 success * \retval -1 error */ static int symlink_copy_dir(lua_State *L, const char *src, const char *dst) { DIR *src_dir; struct dirent *d; char src_path[PATH_MAX], dst_path[PATH_MAX]; struct stat st; if (!(src_dir = opendir(src))) { lua_pushstring(L, "error opening dir '"); lua_pushstring(L, src); lua_pushstring(L, "': "); lua_pushstring(L, strerror(errno)); lua_concat(L, 4); goto e_return; } while ((d = readdir(src_dir))) { snprintf(src_path, sizeof(src_path), "%s/%s", src, d->d_name); snprintf(dst_path, sizeof(dst_path), "%s/%s", dst, d->d_name); if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, "..")) { continue; } if (lstat(src_path, &st)) { lua_pushstring(L, "error with stat for '"); lua_pushstring(L, src_path); lua_pushstring(L, "': "); lua_pushstring(L, strerror(errno)); lua_concat(L, 4); goto e_closedir; } if (S_ISDIR(st.st_mode)) { if (mkdir(dst_path, st.st_mode)) { lua_pushstring(L, "error creating dir '"); lua_pushstring(L, dst_path); lua_pushstring(L, "': "); lua_pushstring(L, strerror(errno)); lua_concat(L, 4); goto e_closedir; } if (symlink_copy_dir(L, src_path, dst_path)) { goto e_closedir; } } else if (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) { if (symlink_file(L, src_path, dst_path)) { goto e_closedir; } } else { /* XXX we don't know what kind of file this is so we * will ignore it silently, at some point in the future * we should log this event somewhere */ continue; #if 0 /* unsupported file type */ lua_pushstring(L, "don't know how to symlink '"); lua_pushstring(L, src_path); lua_pushstring(L, "' (unsupported file type)"); lua_concat(L, 3); goto e_closedir; #endif } } closedir(src_dir); return 0; e_closedir: closedir(src_dir); e_return: return -1; } /*! * \brief Recursively unlink a path. * \param L the lua state to use * \param path the file or directory to unlink * * This function unlinks the given file or directory. If path is a directory, * all of the items in the directory will be recursively unlinked. * * \note On error an error message is pushed onto the given lua stack. * * \retval 0 success * \retval -1 error */ static int recursive_unlink(lua_State *L, const char *path) { DIR *dir; struct dirent *d; char dir_path[PATH_MAX]; struct stat st; if (lstat(path, &st)) { if (errno == ENOENT) return 0; lua_pushstring(L, "error with stat for '"); lua_pushstring(L, path); lua_pushstring(L, "': "); lua_pushstring(L, strerror(errno)); lua_concat(L, 4); return -1; } if (S_ISDIR(st.st_mode)) { if (!(dir = opendir(path))) { lua_pushstring(L, "error opening dir '"); lua_pushstring(L, path); lua_pushstring(L, "': "); lua_pushstring(L, strerror(errno)); lua_concat(L, 4); return -1; } while ((d = readdir(dir))) { snprintf(dir_path, sizeof(dir_path), "%s/%s", path, d->d_name); if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, "..")) { continue; } if (recursive_unlink(L, dir_path)) { closedir(dir); return -1; } } closedir(dir); rmdir(path); } else { if (unlink(path)) { lua_pushstring(L, "error unlinking path '"); lua_pushstring(L, path); lua_pushstring(L, "': "); lua_pushstring(L, strerror(errno)); lua_concat(L, 4); return -1; } } return 0; } /*! * \brief [lua_CFunction asterisk:_new] Partially create an instance of the * asterisk object. * \param L the lua state to use * * This function creates and partially initilizes an instance of the asterisk * object. It also increments the global asterisk instance index. * * \return new instance of the asterisk object */ static int new_asterisk(lua_State *L) { int asterisk_count; char path[PATH_MAX]; char *bname; /* get the index for this instance */ lua_getfield(L, LUA_REGISTRYINDEX, "astlib_count"); asterisk_count = lua_tointeger(L, -1); /* increment the count */ lua_pushinteger(L, asterisk_count + 1); lua_setfield(L, LUA_REGISTRYINDEX, "astlib_count"); lua_pop(L, 1); /* create a new table and set some initial values */ lua_newtable(L); if (!getcwd(path, sizeof(path))) { lua_pushliteral(L, "error determining working directory: "); lua_pushstring(L, strerror(errno)); lua_concat(L, 2); return lua_error(L); } bname = basename(path); /* handle some special basename paths by putting the tmp dir in the * current directory, otherwise put it in /tmp */ if (!strcmp(bname, ".") || !strcmp(bname, "..") || !strcmp(bname, "/")) { lua_pushstring(L, getcwd(path, sizeof(path))); lua_pushliteral(L, "/tmp/ast"); lua_pushinteger(L, asterisk_count); lua_concat(L, 3); lua_setfield(L, -2, "work_area"); } else { lua_pushliteral(L, "/tmp/asterisk-testsuite/"); lua_pushstring(L, bname); lua_pushliteral(L, "/ast"); lua_pushinteger(L, asterisk_count); lua_concat(L, 4); lua_setfield(L, -2, "work_area"); } lua_pushinteger(L, asterisk_count); lua_setfield(L, -2, "index"); lua_getfield(L, LUA_REGISTRYINDEX, "astlib_path"); lua_pushliteral(L, "/usr/sbin/asterisk"); lua_concat(L, 2); lua_setfield(L, -2, "asterisk_binary"); return 1; } /*! * \brief [lua_CFunction asterisk:_version] get the version of this asterisk instance * \return the version string for this version of asterisk */ static int get_asterisk_version(lua_State *L) { lua_getfield(L, LUA_REGISTRYINDEX, "astlib_version"); return 1; } /*! * \brief [lua_CFunction asterisk:clean_work_area] Clean up the work area for * this instance. * \param L the lua state to use * * This function cleans up the work area for this instance by deleting the work * area directory if it exists. */ static int clean_work_area(lua_State *L) { const char *work_area; luaL_checktype(L, 1, LUA_TTABLE); /* get the work area for this instance */ lua_getfield(L, 1, "work_area"); work_area = lua_tostring(L, -1); if (recursive_unlink(L, work_area)) { lua_pushstring(L, "\nerror cleaning work area"); lua_concat(L, 2); return lua_error(L); } return 0; } /*! * \brief [lua_CFunction asterisk:create_work_area] Create the work area. * \param L the lua state to use * * This function copies and symlinks files from the asterisk path to prepare * the work area for this instance. */ static int create_work_area(lua_State *L) { const char *work_area; const char *asterisk_path; char src_buf[PATH_MAX], dst_buf[PATH_MAX]; mode_t dir_mode = S_IRWXU | S_IRGRP| S_IXGRP| S_IROTH | S_IXOTH; int i; /* directories must end in '/' */ const char *copy_dirs[] = { "/etc/asterisk/", "/usr/lib/asterisk/modules/", "/usr/include/asterisk/", "/var/lib/asterisk/", "/var/log/asterisk/", "/var/spool/asterisk/", NULL, }; /* directories must end in '/' */ const char *create_dirs[] = { "/var/run/asterisk/", NULL, }; const char *asterisk_files[] = { "/usr/sbin/astcanary", "/usr/sbin/asterisk", "/usr/sbin/astgenkey", "/usr/sbin/autosupport", "/usr/sbin/rasterisk", "/usr/sbin/safe_asterisk", NULL, }; luaL_checktype(L, 1, LUA_TTABLE); /* get the work area for this instance */ lua_getfield(L, 1, "work_area"); work_area = lua_tostring(L, -1); /* get the asterisk path */ lua_getfield(L, LUA_REGISTRYINDEX, "astlib_path"); asterisk_path = lua_tostring(L, -1); /* copy directories */ for (i = 0; copy_dirs[i]; i++) { snprintf(src_buf, sizeof(src_buf), "%s%s", asterisk_path, copy_dirs[i]); snprintf(dst_buf, sizeof(dst_buf), "%s%s", work_area, copy_dirs[i]); if (mkdir_p(dst_buf, dir_mode)) { lua_pushstring(L, "unable to create directory in work area ("); lua_pushstring(L, dst_buf); lua_pushstring(L, "): "); lua_pushstring(L, strerror(errno)); lua_concat(L, 4); return lua_error(L); } if (symlink_copy_dir(L, src_buf, dst_buf)) { lua_pushstring(L, "\nerror initilizing work area"); lua_concat(L, 2); return lua_error(L); } } /* create directories */ for (i = 0; create_dirs[i]; i++) { snprintf(src_buf, sizeof(src_buf), "%s%s", asterisk_path, create_dirs[i]); snprintf(dst_buf, sizeof(dst_buf), "%s%s", work_area, create_dirs[i]); if (mkdir_p(dst_buf, dir_mode)) { lua_pushstring(L, "unable to create directory in work area ("); lua_pushstring(L, dst_buf); lua_pushstring(L, "): "); lua_pushstring(L, strerror(errno)); lua_concat(L, 4); return lua_error(L); } } /* copy files */ for (i = 0; asterisk_files[i]; i++) { snprintf(src_buf, sizeof(src_buf), "%s%s", asterisk_path, asterisk_files[i]); snprintf(dst_buf, sizeof(dst_buf), "%s%s", work_area, asterisk_files[i]); if (mkdir_p(dst_buf, dir_mode)) { lua_pushstring(L, "unable to create directory in work area ("); lua_pushstring(L, dst_buf); lua_pushstring(L, "): "); lua_pushstring(L, strerror(errno)); lua_concat(L, 4); return lua_error(L); } if (symlink_file(L, src_buf, dst_buf)) { lua_pushstring(L, "\nerror initilizing work area"); lua_concat(L, 2); return lua_error(L); } } return 0; } /*! * \brief [lua_CFunction ast.unlink] Unlink the given file. * \param L the lua state to use * * \retval -1 error * \retval 0 success */ static int unlink_file(lua_State *L) { const char *file = luaL_checkstring(L, 1); lua_pushinteger(L, unlink(file)); return 1; } static int set_gc_func(lua_State *L) { lua_pushvalue(L, 1); lua_setfield(L, LUA_REGISTRYINDEX, "astlib_asterisk_gc"); return 0; } static int setup_gc(lua_State *L) { /* since we know this function is only called by internal methods, we * don't do any error checking * * We expect two arguments, an asterisk table and a proc table */ /* create a special userdata so that we can have a __gc method called * on our proc table */ lua_pushvalue(L, 2); lua_replace(L, LUA_ENVIRONINDEX); lua_pushliteral(L, "__gc"); lua_newuserdata(L, sizeof(char)); /* create the metatable for our special userdata */ lua_createtable(L, 0, 1); /* call our __gc closure generator and store the result */ lua_getfield(L, LUA_REGISTRYINDEX, "astlib_asterisk_gc"); lua_pushvalue(L, 1); lua_pushvalue(L, 2); lua_call(L, 2, 1); lua_setfield(L, -2, "__gc"); lua_setmetatable(L, -2); /* store the new userdata in the proc table */ lua_settable(L, 2); return 0; } static luaL_Reg astlib[] = { {"unlink", unlink_file}, {"_version", get_asterisk_version}, {"_set_asterisk_gc_generator", set_gc_func}, {"_setup_gc", setup_gc}, {NULL, NULL}, }; static luaL_Reg asterisk_table[] = { {"_new", new_asterisk}, {"clean_work_area", clean_work_area}, {"create_work_area", create_work_area}, {NULL, NULL}, }; int luaopen_astlib(lua_State *L) { const char *asterisk_path = luaL_checkstring(L, 1); const char *asterisk_version = luaL_optstring(L, 2, "unknown"); /* set up some registry values */ lua_pushstring(L, asterisk_path); lua_setfield(L, LUA_REGISTRYINDEX, "astlib_path"); lua_pushstring(L, asterisk_version); lua_setfield(L, LUA_REGISTRYINDEX, "astlib_version"); lua_pushinteger(L, 1); lua_setfield(L, LUA_REGISTRYINDEX, "astlib_count"); /* register our functions */ luaL_register(L, "ast", astlib); /* set up the 'path' variable */ lua_pushstring(L, asterisk_path); lua_setfield(L, -2, "path"); /* set up the 'asterisk' table and add some functions to it */ lua_newtable(L); luaL_register(L, NULL, asterisk_table); lua_setfield(L, -2, "asterisk"); lua_pop(L, 1); /* load the lua portion of the lib */ if (luaL_loadbuffer(L, astlib_lua, sizeof(astlib_lua), "astlib")) goto e_lua_error; lua_pushstring(L, "ast"); if (lua_pcall(L, 1, 1, 0)) goto e_lua_error; return 1; e_lua_error: /* format the error message a little */ lua_pushstring(L, "error loading ast library: "); lua_insert(L, -2); lua_concat(L, 2); return lua_error(L); } asterisk-testsuite-0.0.0+svn.5781/asttest/lib/lua/astlib.lua000066400000000000000000000576131242304700500236000ustar00rootroot00000000000000-- -- Asterisk -- An open source telephony toolkit. -- -- Copyright (C) 1999 - 2008, Digium, Inc. -- -- Matthew Nicholson -- -- See http://www.asterisk.org for more information about -- the Asterisk project. Please do not directly contact -- any of the maintainers of this project for assistance; -- the project provides a web site, mailing lists and IRC -- channels for your use. -- -- This program is free software, distributed under the terms of -- the GNU General Public License Version 2. See the LICENSE file -- at the top of the source tree. -- module(..., package.seeall) function exists() return select(1, proc.exists(path .. "/usr/sbin/asterisk")) end function new() return asterisk:new() end function version(ver) local version = ver or _version() if version == "unknown" then -- read version from Asterisk local ast = proc.exec_io("asterisk", "-V") if not ast then error("error determining asterisk version; unable to execute asterisk -V") end version = ast.stdout:read("*all") version = string.gsub(version,"Asterisk ", "") if not version then error("error determining asterisk version") end end return asterisk_version:new(version) end function has_major_version(v) if v == "trunk" then v = "SVN-trunk-r00000" end local v1 = version(v) local v2 = version() return v1.branch == v2.branch end function asterisk_gc(a, p) return function() _stop(a, p) end end _set_asterisk_gc_generator(asterisk_gc) _set_asterisk_gc_generator = nil -- asterisk table is created in astlib.c function asterisk:new() local a = self:_new() a.configs = {} a.asterisk_conf = a.work_area .. "/etc/asterisk/asterisk.conf" a.essential_configs = { ["asterisk.conf"] = asterisk.generate_asterisk_conf, ["logger.conf"] = asterisk.generate_logger_conf, } setmetatable(a, self) return a end function asterisk:path(path) return self.work_area .. path end function asterisk:_spawn() local p = proc.exec(self.asterisk_binary, "-f", "-g", "-q", "-m", "-C", self.asterisk_conf ) _setup_gc(self, p) rawset(self, "proc", p) end -- note this timesout after five minutes function asterisk:cli(command) local p = proc.exec_io(self.asterisk_binary, "-r", "-x", command, "-C", self.asterisk_conf ) -- wait up to 5 minutes for the process to exit. If the process does -- not exit within 5 minutes, return a error. local res, err = p:wait(300000) if not res then return res, err end if res ~= 0 then local output = p.stdout:read("*a") if not output then return nil, "error connecting to asterisk cli" end return nil, output end return p.stdout:read("*a") end function asterisk:_waitfullybooted() -- wait for asterisk to be fully booted. We do this by reading the -- output of the 'core waitfullybooted' command and looking for the -- string 'fully booted'. We will try 10 times before completely -- giving up with a 500 ms delay in between each try. This is -- necessary to give asterisk time to start the CLI socket. local booted local output = "" for _=1,10 do local err booted, err = self:cli("core waitfullybooted") if #output ~= 0 then output = output .. "=====\n" end if booted then output = output .. booted if booted:find("fully booted") then break end else output = output .. err end posix.usleep(500000) end if booted and not booted:find("fully booted") then print("error waiting for asterisk to fully boot: " .. booted) print("checking to see if asterisk is still running") local res, err = proc.perror(self:wait(1000)) if not res and err == "timeout" then print("seems like asterisk is still running, but we cannot wait for it to be fully booted. That is odd.") elseif res then print("asterisk exited with " .. res) end print("\noutput from all of our 'core waitfullybooted' attempts:") print(output) print("\nfull log follows:\n") self:dump_full_log() error("error starting asterisk") end end function asterisk:spawn() self:clean_work_area() self:create_work_area() self:generate_essential_configs() self:write_configs() self:_spawn() self:_waitfullybooted() end function asterisk:spawn_and_wait() self:spawn() return self:wait() end function asterisk:wait(timeout) if not self.proc then return nil, "error" end return self.proc:wait(timeout) end function _stop(a, p) local res, err -- 1.6+ stop commands local stop_gracefully = "core stop gracefully" local stop_now = "core stop now" -- if this is 1.4 or less use 1.4 stop commands if version() < version("1.6") then stop_gracefully = "stop gracefully" stop_now = "stop now" end a:cli(stop_gracefully) res, err = p:wait(5000) if res or (not res and err ~= "timeout") then return res, err end a:cli(stop_now) res, err = p:wait(5000) if res or (not res and err ~= "timeout") then return res, err end return p:term_or_kill() end function asterisk:stop() if not self.proc then return nil, "error" end return _stop(self, self.proc) end asterisk.term_or_kill = asterisk.stop function asterisk:__newindex(conffile_name, conffile) if (getmetatable(conffile) ~= config) then error("got " .. type(conffile) .. " expected type config") end self.configs[conffile_name] = conffile if conffile.name == "asterisk.conf" then self.asterisk_conf = conffile.filename end end --- Index an asterisk object -- This function will return either the config with the given name, the actual -- table member with the given name, or if neither of those exist, it will -- create a config with the given name. function asterisk:__index(key) if self.configs[key] then return self.configs[key] end if asterisk[key] ~= nil then return asterisk[key] end return self:new_config(key) end function asterisk:manager_connect() local m = manager:new() local res, err = m:connect(self.configs["manager.conf"]["general"].bindaddr, self.configs["manager.conf"]["general"].port) if not res then return nil, err end return m end function asterisk:load_config(file, conf_name) -- if conf_name is not specified, pull it from the string if not conf_name then if file:find("/") then conf_name = file:match(".*/(.+)$") else conf_name = file end end local c = config:from_file(conf_name, file, self:path("/etc/asterisk/") .. conf_name) self[conf_name] = c return c end function asterisk:new_config(name) local c = config:new(name, self:path("/etc/asterisk/") .. name) self[name] = c return c end function asterisk:write_configs() for _, conf in pairs(self.configs) do conf:_write() end end --- Setup default asterisk.conf with our work area directories. function asterisk:generate_asterisk_conf() -- return if it exists already if self.configs["asterisk.conf"] then return end local c = self:new_config("asterisk.conf") local s = c:new_section("directories") s["astetcdir"] = self:path("/etc/asterisk") s["astmoddir"] = self:path("/usr/lib/asterisk/modules") s["astvarlibdir"] = self:path("/var/lib/asterisk") s["astdbdir"] = self:path("/var/lib/asterisk") s["astkeydir"] = self:path("/var/lib/asterisk") s["astdatadir"] = self:path("/var/lib/asterisk") s["astagidir"] = self:path("/var/lib/asterisk/agi-bin") s["astspooldir"] = self:path("/var/spool/asterisk") s["astrundir"] = self:path("/var/run") s["astlogdir"] = self:path("/var/log/asterisk") s = c:new_section("options") s["documentation_language"] = "en_US" s["sendfullybooted"]="yes" s["verbose"] = 10 s["debug"] = 10 s["nocolor"] = "yes" s = c:new_section("compat") s["pbx_realtime"] = "1.6" s["res_agi"] = "1.6" s["app_set"] = "1.6" end --- Generate logger.conf with debug, messages, and full logs (disable console -- log). function asterisk:generate_logger_conf() -- return if it exists already if self.configs["logger.conf"] then return end local c = self:new_config("logger.conf") local s = c:new_section("general") s = c:new_section("logfiles") s["debug"] = "debug" s["messages"] = "notice,warning,error" s["full"] = "notice,warning,error,debug,verbose,*" end --- Generate manager.conf with a unique port. function asterisk:generate_manager_conf() -- return if it exists already if self.configs["manager.conf"] then return end local c = self:new_config("manager.conf") local s = c:new_section("general") s["enabled"] = "yes" s["bindaddr"] = "127.0.0." .. self.index s["port"] = "5038" s = c:new_section("asttest") s["secret"] = "asttest" s["read"] = "all" s["write"] = "all" end function asterisk:generate_essential_configs() for conf, func in pairs(self.essential_configs) do if not self.configs[conf] then func(self) end end end function asterisk:dump_full_log() local log, err = io.open(self:path("/var/log/asterisk/full"), "r") if not log then print("error opening '" .. self:path("/var/log/asterisk/full") .. "': " .. err) return end print(log:read("*a")) log:close() end asterisk_version = {} asterisk_version.__index = asterisk_version function asterisk_version:new(version) local v = { version = version, order = {}, } setmetatable(v, self) v:_parse() return v end function asterisk_version:_parse_svn() self.svn = true self.branch, self.revision, self.parent = self.version:match("SVN%-(.*)%-r(%d+M?)%-(.*)") if not self.branch then self.branch, self.revision = self.version:match("SVN%-(.*)%-r(%d+M?)") end if not self.branch then error("error parsing SVN version number: " .. self.version) end -- generate a synthetic version number for svn branch versions self.patch = self.revision:match("(%d+)M?") self.concept, self.major, self.minor = self.branch:match("branch%-([^.]+).(%d+).(%d+)") if not self.concept then self.minor = "999" -- assume the SVN branch is newer than all released versions self.patch = self.revision:match("(%d+)M?") self.concept, self.major = self.branch:match("branch%-([^.]+).(%d+)") end if not self.concept then self.concept = self.branch:match("branch%-(%d%d+)") self.major = self.revision:match("(%d+)M?") or "999" self.minor = nil self.patch = nil end if not self.concept then if self.branch == "trunk" then self.concept = "999" self.major = "0" self.minor = "0" self.patch = self.revision:match("(%d+)M?") else -- branch names that don't match are greater -- than everything except trunk self.concept = "998" self.major = "0" self.minor = "0" self.patch = self.revision:match("(%d+)M?") end end -- branch C.3 is minor version 998, other C.3 branches are 999 if self.branch == "branch-C.3" then self.minor = "998" end -- store ordering information -- if self.concept is not a number, assume a BE branch. Treat -- BE like asterisk 1.5. if not self.concept:match("^%d+$") then self.order.concept = 1 self.order.major = 5 self.order.minor = tonumber(self.minor) self.order.patch = tonumber(self.patch) else self.order.concept = tonumber(self.concept) self.order.major = tonumber(self.major) self.order.minor = tonumber(self.minor) self.order.patch = tonumber(self.patch) end end function asterisk_version:_parse_release() self.concept, self.major, self.minor, self.patch = self.version:match("([^.]+).(%d+).(%d+).(%d+)") if not self.concept then self.concept, self.major, self.minor = self.version:match("([^.]+).(%d+).(%d+)") end if not self.concept then self.concept, self.major = self.version:match("([^.]+).(%d+)") end if not self.concept then self.concept = self.version:match("(%d%d+)") end if not self.concept then error("error parsing version number: " .. self.version) end -- generate synthetic svn information if ((tonumber(self.concept) or 0) >= 10) then self.branch = "branch-" .. self.concept else self.branch = "branch-" .. self.concept .. "." .. self.major end -- special handling for 1.6 branches if self.concept == "1" and self.major == "6" and self.minor ~= nil then self.branch = self.branch .. "." .. self.minor end self.revision = "00000" -- store ordering information -- if self.concept is not a number, assume a BE branch. Treat -- BE like asterisk 1.5. if not self.concept:match("^%d+$") then self.order.concept = 1 self.order.major = 5 self.order.minor = tonumber(self.major:match("%d")) self.order.patch = tonumber(self.patch or 0) else self.order.concept = tonumber(self.concept) self.order.major = tonumber(self.major or 0) self.order.minor = tonumber(self.minor or 0) self.order.patch = tonumber(self.patch or 0) end end function asterisk_version:_parse() if self.version:sub(1,3) == "SVN" then self:_parse_svn() else self:_parse_release() end end function asterisk_version:__tostring() return self.version end function asterisk_version:__lt(other) -- compare each component of the version number starting with the most -- significant. Synthetic version numbers are generated for SVN -- versions. local v = { {self.order.concept, other.order.concept}, {self.order.major, other.order.major}, {self.order.minor, other.order.minor}, {self.order.patch, other.order.patch}, } for _, i in ipairs(v) do if i[1] < i[2] then return true elseif i[1] ~= i[2] then return false end end return false end function asterisk_version:__eq(other) return self.version == other.version end config = {} function config:from_file(name, src_filename, dst_filename) local ac = config:new(name, dst_filename) local f, err = io.open(src_filename, "r"); if not f then error("error opening file '" .. src_filename .. "': " .. err) end ac:verbatim(f:read("*a")) f:close() return ac end function config:new(name, filename) local ac = { name = name, filename = filename or name, sections = {}, section_index = {}, } setmetatable(ac, self) return ac end function config:verbatim(data) table.insert(self.sections, data) end function config:add_section(new_section) if (getmetatable(new_section) ~= conf_section) then error("got " .. type(new_section) .. " expected type conf_section") end table.insert(self.sections, new_section) if not self.section_index[new_section.name] then self.section_index[new_section.name] = #self.sections end end function config:new_section(section_name) s = conf_section:new(section_name) self:add_section(s) return s end --- Index the config object -- This function will return the section of the config indicated if it exists, -- if it does not exist it will return the table data member with the given -- name if it exists, otherwise it will create a section with the given name -- and return that. function config:__index(key) local s = self.sections[self.section_index[key]] if s then return s end if config[key] ~= nil then return config[key] end return self:new_section(key) end function config:_write(filename) if not filename then filename = self.filename end -- remove any existing file or symlink unlink(filename) local f, e = io.open(filename, "w") if not f then return error("error writing config file: " .. e) end for _, section in ipairs(self.sections) do if getmetatable(section) == conf_section then section:_write(f) else f:write(tostring(section)) end end f:close() end conf_section = {} function conf_section:new(name) local s = { name = name, template = false, inherit = {}, values = {}, value_index = {}, } setmetatable(s, self) return s end function conf_section:__newindex(key, value) table.insert(self.values, {key, value}) if not self.value_index[key] then self.value_index[key] = #self.values end end function conf_section:__index(key) local v = self.values[self.value_index[key]] if v then return v[2] end return conf_section[key] end function conf_section:_write(f) f:write("[" .. self.name .. "]") if self.template then f:write("(!") for _, i in ipairs(self.inherit) do f:write("," .. i) end f:write(")") else if #self.inherit ~= 0 then f:write("(") local first = true for _, i in ipairs(self.inherit) do if not first then f:write(",") else first = false end f:write(i) end f:write(")") end end f:write("\n") for _, value in ipairs(self.values) do f:write(tostring(value[1]) .. " = " .. tostring(value[2]) .. "\n") end f:write("\n") end -- -- Manager Interface Stuff -- manager = { action = {} } manager.__index = manager function manager:new() local m = { events = {}, responses = {}, event_handlers = {}, response_handlers = {}, } setmetatable(m, self) return m end function manager:connect(host, port) if not port then port = 5038 end local err self.sock, err = socket.tcp() if not self.sock then return nil, err end local res, err = self.sock:connect(host, port) if not res then return nil, err end res, err = self:_parse_greeting() if not res then self.sock:close() return nil, err end return true end function manager:disconnect() self.sock:shutdown("both") self.sock:close() self.sock = nil end manager.close = manager.disconnect --- Read data from a socket until a \r\n is encountered. -- -- Data is read from the socket one character at a time and placed in a table. -- Once a \r\n is found, the characters are concatinated into a line (minus the -- \r\n at the end). Hopefully this prevents the unnecessary garbage -- collection that would result from appending the characters to a string one -- at a time as they are read. local function read_until_crlf(sock) local line = {} local cr = false while true do -- reading 1 char at a time is ok as lua socket reads data from -- an internal buffer local c, err = sock:receive(1) if not c then return nil, err end table.insert(line, c) if c == '\r' then cr = true elseif cr and c == '\n' then return table.concat(line, nil, 1, #line - 2) else cr = false end end end function manager:_parse_greeting() local line, err = read_until_crlf(self.sock) if not line then return nil, err end self.name, self.version = line:match("(.+)/(.+)") if not self.name then return nil, "error parsing manager greeting: " .. line end return true end function manager:_read_message() local line, err = read_until_crlf(self.sock) if not line then return nil, err end local header, value = line:match("([^:]+): (.+)") if not header then return nil, "error parsing message: " .. line end local m = manager.message:new() m[header] = value if header == "Event" then table.insert(self.events, m) elseif header == "Response" then table.insert(self.responses, m) else return nil, "received unknown message type: " .. header end local follows = (value == "Follows") local data_mode = false while true do line, err = read_until_crlf(self.sock) if not line then return nil, err end if line == "" and not follows then break elseif line == "" and follows then data_mode = true end -- don't attempt to match headers when in data mode if not data_mode then header, value = line:match("([^:]+): ?(.*)") else header, value = nil, nil end if not header and not follows then return nil, "error parsing message: " .. line elseif not header and follows then data_mode = true if line == "--END COMMAND--" then follows = false data_mode = false else m:_append_data(line .. "\n") end else m[header] = value end end return true end function manager:_read_response() local res, err = self:wait_response() if not res then return nil, err end local r = self.responses[1] table.remove(self.responses, 1) return r end function manager:_read_event() local res, err = self:wait_event() if not res then return nil, err end local e = self.events[1] table.remove(self.events, 1) return e end function manager:pump_messages() while true do local read, write, err = socket.select({self.sock}, nil, 0) if read[1] ~= self.sock or err == "timeout" then break end local res, err = self:_read_message() if not res then return nil, err end end return true end function manager:wait_event() while #self.events == 0 do local res, err = self:_read_message() if not res then return nil, err end end return true end function manager:wait_response() while #self.responses == 0 do local res, err = self:_read_message() if not res then return nil, err end end return true end function manager:process_events() while #self.events ~= 0 do local e = self.events[1] table.remove(self.events, 1) for event, handlers in pairs(self.event_handlers) do if event == e["Event"] then for i, handler in ipairs(handlers) do handler(e) end end end -- now do the catch all handlers for event, handlers in pairs(self.event_handlers) do if event == "" then for i, handler in ipairs(handlers) do handler(e) end end end end end function manager:process_responses() while #self.response_handlers ~= 0 and #self.responses ~= 0 do local f = self.response_handlers[1] table.remove(self.response_handlers, 1); f(self:_read_response()); end end function manager:register_event(event, handler) local e_handler = self.event_handlers[event] if not e_handler then self.event_handlers[event] = {} end table.insert(self.event_handlers[event], handler) end function manager:unregister_event(event, handler) for e, handlers in pairs(self.event_handlers) do if e == event then for i, h in pairs(handlers) do if h == handler then handlers[i] = nil if #handlers == 0 then self.event_handlers[e] = nil end return true end end end end return nil end function manager:send_action(action, handler) local response = nil function handle_response(r) response = r end if handler then return self:send_action_async(action, handler) end local res, err = self:send_action_async(action, handle_response) if not res then return nil, err end while not response do res, err = self:wait_response() if not res then return nil, err end self:process_responses() end return response end manager.__call = manager.send_action function manager:send_action_async(action, handler) local res, err, i = nil, nil, 0 local a = action:_format() while i < #a do res, err, i = self.sock:send(a, i + 1) if err then return nil, err else i = res end end table.insert(self.response_handlers, handler) return true end -- -- Manager Helpers -- manager.message = {} function manager.message:new() local m = { headers = {}, index = {}, data = nil, } setmetatable(m, self) return m end function manager.message:__newindex(key, value) table.insert(self.headers, {key, value}) if not self.index[key] then self.index[key] = #self.headers end end function manager.message:__index(key) if self.index[key] then return self.headers[self.index[key]][2] end return manager.message[key] end function manager.message:_format() local msg = "" for i, header in ipairs(self.headers) do msg = msg .. header[1] .. ": " .. header[2] .. "\r\n" end msg = msg .. "\r\n" return msg end function manager.message:_append_data(data) if not self.data then rawset(self, "data", data) else self.data = self.data .. data end end function manager.action:new(action) local a = manager.message:new() -- support ast.manager.action.new() syntax -- XXX eventually this should be the only syntax allowed if action == nil and type(self) == "string" then action = self end a["Action"] = action return a end -- some utility functions to access common manager functions are defined below --- Create a login action. -- This function creates a login action. When called with no arguments, the -- default 'asttest', 'asttest' username secret is used. -- -- @param username the username to send (defaults to 'asttest') -- @param secret the secret to send (defaults to 'asttest') function manager.action.login(username, secret) local a = manager.action.new("Login") username = username or "asttest" secret = secret or "asttest" a["Username"] = username a["Secret"] = secret return a end --- Create a logoff action. function manager.action.logoff() return manager.action.new("Logoff") end --- Ping action. function manager.action.ping() return manager.action.new("Ping") end --- Status action. function manager.action.status(channel) local a = manager.action.new("Status") if channel then a["Channel"] = channel end return a end asterisk-testsuite-0.0.0+svn.5781/asttest/lib/lua/luafilesystem-1.4.2.tar.gz000066400000000000000000000615001242304700500262620ustar00rootroot00000000000000(I=w6+Hw(V876}۽n 3EҊ~3?͵[X 3 3> B!2~;pÞm>ɠ?89 O&S0I"fNOГw~d↱CwD.H|Zt%;g ߎ%e~g˥HNZ,HNZ&A6\w|՛:44#4񃄵TX- ́VEknhƼ9fxaP 2Ps} }薉s&GѬ;لuCOp4O˯|?_Aҧ4,Y _X+[F F CFH(Ă,$ 4Y#lq)'BM(0^+-,p(s 9wx JZ^"xh Xķ͒VӴ:ESs4fA``EOL-qęd`ly|V2 S& d (8:[=27 a`i bB,5̆ˮRz ZF ^N#S/)%hXѱ. Īă3CYYQ&L=^ONk&LkwFϜgQQZ KQ!+uz慱m.$x,Qj\:g U|oMb!A9Ywq!YrqY;K5Zq֑:uF[D.9u0d<j`uZVP v7K: IjpG'+33KQmr !aUkFw'q!O/ڡa1N*~?d*O. #v$1ka_kdϵ\jASo_f4y{x>4M4tZO[ JTK1((\ek6j=dK$۪1~E9su5Y՛v(x;2!bŜ£Y'.)0ꉔiMƿTjiBh(c8@̻j|1ijAv-\㒁/Aw)*OQLD--P"xK(SB`;w5peI;A(0)p>d'^؆\Ds\Y}3,ΛN{Ӵޝ|~S|6? i^E ,ԝ$z`xzѰwz~S|F|sq_23_]0u||_^}o_~}८;c`g3]Vq7Nf[cO}y$[O}g8a$={9,+!?n uƿ$&#B;2]υH6OĴ4X"Y&@Cx t=)!˅28Ba#=w.C+h@Bt/9.0&rpS2r$,΃H$}3/upʽ0Ŏ{PGbxx0xN1t \0 < f=fU#lKL;1vg?{LL1~t}X` $3!/6zѪEHm.NoWヾd@; h89d9MguijvcG$X97 \h4ǫ(9j~P?Z] +?H ug/4T u*č\}}h\6ߚAZa$ BW{vd3aů= 3~k.w3@ZR, p'E%:Ճ܁-V&Mw#$Ζ`O gӓ:{Öޢfxůֆ'")4g__dOQ驅fAvc.P_h =e/xlQsG`,Tn[KܓaC=Yb\=/qjJk Jp=UgOZwݑlB#hl״EJ[-#Iuym@CD2fkaȅª ;[eVh&aǔtNT }&c6TRDCDd>A\ 10H\[@-n+gTk7"_X\},Q~(y/ȥ<ȕhelg hELE ۺ9`>JyFPmh"? IC͹@ HXTݐ>b3t&_+?fPfD@tSPF(Z0AdDAwa &M+Oʀih2Mpzݕlw%6n w`ZɃ##0x5Ei45wAunFH^ ˯R-I.ؖ>Zӣ%`c]<8)2M tkBq+2όa5 eM ,Vh 1CusGZx=C`UOJY\#_-l+ GbmPw$ BsSBu1ݿ X SK .}Rx_1$PYp+"RHm}aU So cB6-5%mЌ#Q֭)sA(o] mK YE/B+`tG*zMtN8=G12(MK!ੴ@pfBUҎm;~qH3wXj 1sK>Un7"C2he:h< \2O7# ! 1y@UhݖCr8@`6?t=HmuSs)5C Y .c˥ 'Vnt̬<-]ݵ D?H! Ĕ"j )g;Q%{@Uk_t1 k+|b2Yb7sȶ\Y?±G|?w7jaO{a U0I ~^)q<ʈT>FH!Pk[Gebn  2$bTeSa-A|+O`%D\ Y{ I#yI )kbi2Bu\dZ١(KZ&~FC>u _Uw=ԥKzkBIRۂmks9)u k;t!l3BԬ+jXjrh,|~ 6mNbSY&-5hBVy(I.H8N]9) W~ R]&X` XwYv3WM񇮕9SJVEx}B:2w_9'Mƴ+ejZK.2j9xn8`~tnv\жt=AVb8c[qZOT'$n2ReyQL#^IFA[ xW>б؊mAm94]*@e}kL䀅A'?&Mhtu_υnsXYSƉcSȍ. Q`".{\!R}p!,n4}5Y@W+lK`5>[ME|Q{,B?Q+@TP0Le;?J?_wf~c6 _t1V8i 0sb{qP4oPQFޱ㳟 *.8@Zğsh>ĸA9 .gOկ~;>H4G5T)Mxsl4a|H'mLnSo>^aWF̃$tCȭ U^ N]{Ol 2AV{),ًF^v%r tE"gu}hРA|AJȄJAs8)+Gؚr eXi"2ӋZSL/2L$tܡ6OUe{F9Dܠ֭TzaI3)bQԹ:5x؜`wcXZ*Ul=AR?Pw_#MyE(V`MG&'5W1PQ(|l f}(ӍΊ|jy{IL ;.7wAtB%G рbG^V=bg#%"Χ&Bh&HЕkx޼3QPST_. 7nB`Ae MP!1KbqL^S-\hʹW"h,2 m#ŷȔsHYJd0T6RW"$|.8Oمً*^|K03˹*7tYeJ8%S: f)sWtk%PgkzK ?i|M{st]}w\K^iۂVbf!x_0O)fF<1Q]MNiD7 Vf|i~p~H@vai>s{ػp(e !T&r:f4f43 )?]$InQBr+J% QQE$J~gP:;}"̻{{~`=YGmm-6r 7\⿿⿿⿿   /'࢓ZW{lwK`#9y?f"(+3K|},lLsa;:3؉ .(`TX3 q* Wz {MĈP~@w=^ 4;zAp ΁ skDX. x-b.̝@obΟXB-?S uXRyAJȺy("ل86osowqO`JDg#d} قϠ؈Q-q')~\{;x̕_"5\_EјM0sCX1j F HXpS~8|`)5kIYI`vbCZ3Ps5:x#v,́/ĝH`YY\H@ 愼-g\~ܹn` 76bmW,@'N.'}~z1Cx lN&YF<U`K<GEd j a ށ'Vd,0̜x!ahrD&TP)#cA9cl<OVT"R1cT. ds $"hLTfX"U P`)8 "ZOfϐdnMƛ0D0’)(,hƀe2$`f* ce"ZȬZgò>0DƐ'T2U$SZ)X0!da'(Xv-E#AwQb@]y}X,|6>Z]Sqjњؽh_~44:ZKKkƍhHqEM\/'ML oJ9`Ei-Z/ +PGb筃ua TETF+f^Xd9`8)L >+tffOlx-dͰԫ m_G h@5߾&qڈY2^v)ŧll5_!yF S&&cnk,Qe)sG7^,Ui)LF%Y`=IWiդ@T'$PYWWw1{{{̱_nC9L5+V${DɓhmFifv2+km"[Ƿ^Y(lmMK<9 ID\t)=]N[ާ,[0֭[wUSSL/-=+*Fc\kהpr^ ݿuFknnגvt|v(x*Obϳ/^ȳN|!Ƚ{ΫX?roI^*(?*#-4آj&y8 mhj665D|-)-YAΙ10X/gJժPB||qIw.]sssr@\piccC&d=-yfI7#---P)5YYY6yh0jt~-yK-jY_i f_Y4ctsrPђٹL%r:CL 4PPloTsH4HZSJ8ߪ,--mmmKd+fyxxdSKvɨ=jfgL3c555w\lR=㚥],]uw?{K]ڞ999]iAf``&x0;8RZ}}շJ]*#Npqk fgu9SyӏO>%ŞJ-hVRhll|黱s:+D1ys(0@N p.Ko={p(-c~>:cƍC]!cok.o=σ38bbb\]۲ehRyĵ$}>tݠvﮚ%=EoDңfDdOGGGrr[Ox\8(}U4kGٖ U)]} Nq>?^аLFHT}/„0e"\S333['>Re!Xl\Y/y||na/HcUx>'+ 腚oxX՗[¦s<왕zv!5T̫O'r7rؙD%J@G (Jhsxeի[Q8-^q݀CP|moЇd''bCC_0_v-/{j\:Xrka{\ $*_k"&.{1) D+NFƣn !!P:y̹s{q7]tKkDmɝ;wܮ?()``(q=kj:{|Sokv-prap&;P8q'pP ))mG>:J$`;2N=p]wVJI@ "Q7vΝ;WnBGMSn6Ȟiٯ:o<}if]TB 4.~٪u]Hȣcj}ӑKUbq0/9* 8p X/+*^}h*2!-_$7L5$nswT[t_ъ 7%wnx_M٭^7'2B>}zXDQ|_zuGPwŏWjc:_K lhrfgmF&BrCyslt_Rs'5.Fٳ Ne$ 'G;'16>NfoR^>Nca *O==̄m|]C`GAM/ղa1盚\R /Ǭ*(()//yҏ)218|sR26, /!j}o )pa/U{{T?TC;:|75/gnҥ_k.8&_?|̪@DDI O\l z%y֒΄u|}&T2pu'dg͛U?q@N`8O(;_I]/99/*˸iuP$;JdōJwP4sz,q>u]wXn~~'`Ae"?F7 ȱڼA2?I6DqAϺgĵYrƛ*$e$2i_\"jb aZ!c)-`P uEev2V(UxXħ }[tkQ-U0z%Q%[$5 uWylwwMNwxxDK2#'"qĈw}~tu6Iqlj_/S󘳲w OJ.xoUj(3fwޒس'\ŝ/! X Njygɢ_eSKBܣ|jٵX8?1:Ժ,߂ Y&+wr{t_Zz}DJݬUϝ_,z{@90qSM[x]?4h.61ml+<^g;H4Yyh cCb9%sܺvlllݹ{=6n2(uY^'tu*;& ^/8>hM YyvE7馼oZG#Mvm\G^Xyp^yX%`(knDzXl|sJu6眖 Ǻr]2헼(9u cl 丞3YL&3/99'z> t2>WuiC[GYV;$}NJus}R@O[׸GnYs:}Oo&EcCƨ1]j陖m*"r2]~U%$kIl>T#hQz;3;_$p!;EXsa6LoqM~a]^?)/*f͡g_ ص`Gom]I"tN|}X}Q~(NqmƛL'JN6}K2([axV[|^0D..qe[OK>mxU13;<ݓM|sC"mlgo۸'/6ě5u{1hOnέG+qܰwK=\\\3&V=ia m93oo׹&@I?)P \SMYl٥1VK60/Hq{~=(/:ޒ9C֎IO}Mׯw .sd\Ȁ;H-c F{xjq5ϙOXtII^oǷZݻD4Z&d!O 4ӳ nl@-酇g|9zބ6Zw^}}}ddo=._"Z*?NN /AWf36Lx ؕǣw쿺|<^ ={* {*[j2_͘| >5Yk +H[xƌa7ʹe<[ܤmyjH4O(D¨u%,*Aܞ皛yWDF; ?LC–[WK #K9Y'N}=ȶ kwqhkBdܝobjFGq<=EOL̑cԞYMYj?6=g~Akt^P9 %&q~^!g+Uo|dNU[4Ez.3z-8zsvʞ A}X׬s'ۆXYYLŲ pS~&GD̛ 2c W<@c##j+=I? ^cuoFgCAVWN>udrUhai3-̤y{*ih=^hwT~_m =z'o1t ȏ{t)߈[/*49vi`+N]UB{q}i9,>~4hOn_. mh߹V |uގ ^paٟC{f\/ =q\׮.]pҥs \\`!0u/c &sܩOUV˯:jLm޲W3W8p/=Bo3N387o<|ދ zxr]=K}4>񁼼W꿼Nj65Xa'8xH0Vu_3;#|3pSg lJlXń )yg(:ځزk׮\=x0¥s=jQ\a8nY9nӢT<^&>zLۋS~.Kx}#`DrrO/Qn4^_ba=ʂ VS-;<ȒMELN %i5UUYS.2w{@DҾzn)wŸ*vEuS7L ^ICq=sqp?gǗyy R|z8!00ҥKF”ztB99i9XO~i=-fzz3.ﭬ,YZ{ZA9"Ll|I->fegg>Ι~?6 iVԵ0u.*> ,P;y^NIGgf6j_&5pTf)6JMqKw=yOjժC)9~yu52}pkc׭J}MDV#iɑ_%K,a6d}s['Io& XriHqXt΁Lf佯($%͊Y>ei`zttWMOzzF{2 7Ooc#Pdmm/VUEV,cm:D=({`Z={/%̝tx-[jWQ7]˺2CW^^ziQ˗ZZj+#lAY6xX||<.77n͚u:n;J~/_,//tiwYR۬ `vٳ~֡hX̻e<cV$Ė₂?ن\=xG!o`I=-~t/N-G|\Ԏ'ܹT0Wߴ]'ɷ}-PH#""{gqIII.KWߠӋggYsܹt\L}UΓ\ִ"XU}~ZwdxD"s666~S(45{|=paϞ=W91DeŨQ&}RVsn-MXqt͛3&2711q{MX㔲e]vl8:Cv8ƪU >wfrϷ2Daaa؛OiSݻt"M@z8ɦk kig~ m- ]sk>wkFIeĭQg73g޵Ӽ$giߚ'}.l7Ǫwt߽N+;`(Cz{W (dČI 4 b|vI"1$Be$y~z'nW?(;2<Ch bkO߸L%jGip*qrb]+,J*%x0YJy5vFpèǥ l 7ê!YpGVƻvjUut.炅|@&vPtD, &4DK0yD=YBr"ћ?K8b4\)Jzpy`4Ra.Z0EрS>/*b9 vwQ(cN!'$WEBnݕ-eQRTDtR s64ܠY-KLxy@0_p3|$ pd  y<t%*НT lMwVҨvV_=ͳu@&ook!/7Tb99Pw?P֎J_ab3b+{HCL_x=6\*؈ooGJ+ x#D|18 %F%U _&/pڨXY@E [.Hm:5Fꊵb-Bpb=X ^T9wOz2ݰ{Hw?y8n}}\lπoӃ/W>"᪋;JOsŖ(6lɪzV.75CwM+W̒AbƋhE`.!b l(v5_]ξ.4c:/Uب^褱='Ab+$@LZQ/@[2T>⋅.?+Se *8R@sU*T">2[ Pk €rp1ǑFGZ}ZT8"h^Z A) 1 >WU~u_/#` w' @*LܑG4(ljdpP %:|{  A;1پ̿ pq7e僲qN/(Q."W835w|v6"}>YmME%ڗ ya͇s5ߕC7s@"YDžvsh|qqqV5p77tƌS|_ $"O&Xz,Xz]ӁBͤpZRVVA A% #dVɏA ( yhr0Kk*MpP) CFT $@@6ƀ'QLB0WAS%ӅPL ?V{|`c*SD[U-Q}::oGT S/…Ru9fȣPB9Phwr @Ynqr}Gl1e^5Rj4 N"{0\|> .9pHhD:"Xa! 4HT u|kA?"kK6)x2݂/ < %pTb^oo0WG߆bKm/ʗ?ʥP܎`§#\5Q m.Wh& =u顧 47cNwઽminM@iU)/\[ZT9[ (8?TmL>ιFP.-|K8 hTs\&!\p$J*U (Q4$#YvVԨΩ-إ_p죦ZJZ :^uBhurWEUmMxH \A'ng:؀9̟X O K4ncmݸMnnjtP ]Kn`m MèZPtDB_mDiQp!:'Eծ5(:UF}mۤ"ɀ\6*@U-n("աY7 (uO0oׇYň$ Ri64*$}#>i>"n̓)g4كT{Tp@]'/uOó;3w&'nu$t17@"{H€ko5Y8E#9o\g߆B^Ҿ6'4K=<ʏ٧4YO")qsr+ tZ[%i̇6PҧYh h'4xapbaکJбJ+b)B,hi[gG6jJsh5ibi CIPR<\[EM &RjP:VHk'ḒH݋ydLxXL?/bgXckJhO@`X.@`}/Be|,/ Ukf`'!G`Hœ`xIdW7 UYrkBH SLt A]qa3<]]8ɭTcхm(Oki㝯D[ m QɭмЏΚ2%6Q7W4h`Xbo ]0ET hB ۨ^[)БӋQ&03I/*%0t`G|Du0 Ǎ2U Lf!'{٭^pP,iY.JE|lm\՝j:P-Gg <:?ǃC5y=tPTJl>(E@|z9/JH8t>KiTT@W5"A]z"4D0887c[_U#HEKE|Q堫yR1+&⧢";6B['GR3he{hb^?f{;{Q?<AS fD сa'13<@0%DڡN2[.^ZQ/1*x%zd%$-{<U1@901gF" Y]>"PPT0M-!ʔ0Ha6uR(V$[ՌCGfw#A  [=hXgNn>CI)-'AxS`~BB`IMQs=T[~N wUMPm =ڪV=w]VO3pyBzXQh?ii:Zo4FfZϚ^p~aQAa^ aY2@~QUTX"TstK(qEXU32:1桐fI3#\ EasjrR#1h~N"G@UkRfTCpU@a?`j ` .1@FmRP evGh2)Pz8s[8t Şa{L~FG zQ*'90}a= 6 &FnGIQ E{ŀaMn(9Hυ -Tidhf "d EՠJ4 BJOg6ڞ֜Onʁ׭΁Gv@bDP80ȸ -*V@Fƃw}R  ٯɁ٠BA(BgFSBl~'"|U Oa/-hpW.;_$Ju4}1[9(2@~>OƆ0kl2(S@wT:hNW\Vnʅ7 qPHd.CIX00&| 28LOWL툀"Xy8@ Do8aɋ-P@4 (aXc- !tJ?ZWBQNj%@TLi?ZgqPdu` *ӱt#̓r$u"٥UəkehwKiӫHj8Goaq8<%jI :PMX 7Wo[W2? wzyҫxd\SVQ ̢!CBZ>f C,/G]} ]"SgobѼ53&,Bt8+@]W@bG#>xkLo0EXoIgì59ib^f92$CݱÃgg%%4ɵ h5vpE*"^*i0xB#!=uvͻγ]͸ܞsjǴ`Tu -;kB ~Ϥ4\3ӺqmUH'fHHkچ0U8P {l9Dv‚qw0l?1"ŽRV6~Z%dVxD 1ɜzJ@!~M$Y>i/+o8v7ZwMn>)6x"bb1کb0Զ7+J-n'ŚxlSvh aHiW@cXf93P}}Si"Z ʌ ?bC\[b&0<IjI-ck[fhqSIO ֚HzՐU K,a #5-֔{ sp-P/W;ړSB?As:bb ο|ႀ KeT8;NüòF*T#U20R* ((ɯQ㍬TNs,] h , ;U$L5Tk3Sˈ TULlhڍM"46iX8A%U\";KEJ+~t5ξWa>LzpwĂZ E< \ +2_Dc\T)*TU" p `i%PXs hL\tr7iPPqIz)#c\Dg](Θ?c\F]i&"%(֐k.ejT刐 GV;"i-u_vv^BYnI jエhY3V2W ZAT|\bk1^398?-#+7+ił$PtZce4(`,[JX,n+4FAm&3eNNHB[7OV0|#/X)P(P9Ҭھ87}T |8O*>h PDF.Qa'^63d0&͡q Je=lU_y!ƞEX E}yɍ@#;[*u=q〳}0uK S"Mh5|X9,h8x춛ɛ+$@.8F0ly7tG(ZmFq5J?>?'/^cer}jmѪtjvۻP]`7]'qK+VF#t]&bhdhe %:LVX8_/U ,EfWUL̥ VN2M 2zyrc:G: ЯF_fEAҨ!z3c[W:h #񯃛qn_o Ѭ=I;vbsUq!_bz*qWdA:-J?og*~ _@*e 9xJz!/Jw"Tj6Sy) tzky- qd2[*cwy;{fΝ3 #P0yz-i{_M<{*_(#@|wVW<7GᏟ%:{gOy?=G.!7T (R6ؿR`{ F.1ΟAI0xy X}ӫ]lgKLӻ/؛(VEذ&͆׮M}!EzԎ=$k7w'_N^cPK wl4}o_g:Jz5Xlu*"7(O2?p7ӧWe4VݩIf]mxUO?<~}] ꭏIi^x@ @Re6FF)SM^&YrF_֬6u'is?h[Qx Poz _vKh-)A{;|)oO~j5Zz:IHgH.^GJmZU;I]'i(&6~_o?N.f?xNlmˀ?w) :fM6Z Niώa7d=t@^ڭj#tknjxvuGW!=Q.@T?6:d҅O\,=\UopZTbۓW6J|:KqZ^-#)#8D{~u|J^$Jx pt%:΃~~^70I'|'|'|'|'|'|rO asterisk-testsuite-0.0.0+svn.5781/asttest/lib/lua/luaposix-5.1.4.tar.gz000066400000000000000000000321161242304700500252440ustar00rootroot00000000000000HuL0 Ed]V7׷F&@uBi.HS|'q \qk\)~Gf{L %2_2;~lW@ s~,)bY7PLΗ$  e7ۙ>zpmN*_W><O8&A>MmNƿG'S̹R֭mwȵF}Ugrefr' E`;ķmAVy4cwzn|nQ0PaI2O5eb3}4uIP2Úf8oԉbf]mV=Q[g//. Ih  *8Tl{h|лi| ]mpr!@^{ґ 59^b8eHh/5'Fp>s^0Ľvl^d](f) 6<<=` ݈6X=oSl(4wb?5t\h1#+BCc\ii 8TbɠoOƥŪft~/.%1 (Rd%F~b29ݠユoE餋%G4>/~|68<oEP2Dx_ I6?0A]xW(uyOe]>~3BaP u U43-"HTfRէؤqe6r*"'5Ɩxjͮϩ(2[$̮ǹCj$2KYP; `΅ |JS>A/\Tq?ƞXa*#+W&p qt4"i,U$4{&~p 5l'GDF]`\Bcx|;VʝqX|=gFJ/YƴE edoV? Z5܊l}3(X#\õq&MI|A<š/J+4 r>kPgWlt=%aP }zZΊ1ꉀ)'L%SQP슝bQaU(-:_qz:2:\``FL<bĨ4f zjϽ %Vi_B,B `h$Gu 1Bi,"n2s=' gMS%)Ck$K%]kfQsF%)i$Z[Jhvdz).?0Wr+<ǠbAW󫛚g‘H.+5#UAU҅'#uLWiŮL || 3(p?9c_Z&+rdlҘ7˺Q($$"] nm5+|4Y35M%v%BPRU~"UdV+ CL(F~HY^$˺"|W*A*=ᢊ^]QPV2(=׿֩8X[`4Zbx5\}sW5b5 7m\_mH,,hr`m;2x\*uD"%ٮZ aLw4 XӲpH\qnG͐O{$3Ѡ LسAYC֍+Q@[ clcAd^(瀝!%[NE 1S 33A[wd`xP1qUQ-*Q\Rr(ō@͡]Lx: l\1V6X)ՈnX X8}! AG~#p؄7Dٯ_@tnbr:<=D"=R Z./hW,+9)x85(L)9Wi.K2QTZ}ѓUlpH4KcX b]nFU2]W'])U'WP_<nɮo -OmLn9e!mf< H?GGFQ0b}+؀=:98DC x.c 4Qq˙ qB~>kZEic :(e^'1Km27OJ M%J=."Es׫cD1@)2\x!IIqDbx>Çv,)`NX҉!:jB1*\u̝JjR0Z0L|α4<z^i=%|v5Ѽu 7y!@h/FSD "vbښ EB`]VG/T!5)f) Q}*]*r? (`T ^$] BB* T` vݭ-@pQQt^ov[0:Wx)#R`E[ I,`fl&M~ճMhUvHBtfB'm2TJ\!˝Ǐ#;=UF 9VE'TRkV4A.,ikw$EDulq&oeȧ$ɗ/Ã_;wl瓠 /g|wyRRyY{t}zު>{~rzn#U"V/Ve~TJjS9IyY`D"*['bTyX,]mXn.1N&v-бR)Ae#88#d%y5jOAFAr%5$RH:M]X4׫?=WI6#e%^GTkc@'P9#ʹyCSTb` F"qBmr`$Lܢ`lcxѣZL;I69-HlFԵПʉOځRศ@G.J?Ѧ>$Q"D3 XӑV H8>%Wa`+?m&.ąca`Y|F ܶCZjGVdb4)塬,7Z*hvtS9\ PZZ>5jJoLQ Xqk/S#< ڻL)Gcm!/ܬ Qv*MfN&tϭ%Q:Da& @P/D|0 Ӆ~/vYx;bkhA&lKȢ5~,ņe:K"x"KK;V Vnʩ඘K _zs y|(<D!@0Sj>LYXh"4 b+ C| t ~#I7QˎE-llPjGȏ@"Ã'^I!`Uή.lZ>/ۭO*x=*p=*<\j_Vkѿ/«4vHTXY 1oYU!-^7*1*@k3QsCRr*sĖЪ- =сmگ0Ycڪ?`4Ї_q }!cYu1˧bfv*gw3ի!D =;dU`mxťgjH@/% DdڸKTv~LBdv#FHs_V4BY\hjzl7,Tx"Q? V݋X^اJLsXF_U77mlo)b0TZU%cU%^JOJ9^gڤl5hn CP+hI|=,#E*" _t+,p]4_)^KsKߠ4ZT!<^'dWZz,_q/1>E?2EVd gYV1t*MR+Nת$vz=k>7_8x[&8V_P[+TIs Q/"kx:ݺXe}27`]%YbQ(nRӫqglN?'g>1"*kIi OxA+JL`q|kRkw`$1_%XGz֘'F1+g۸ "$|SvZH ݫȷwJBU3$+-2IU^5x WmщXFub5=2=lMzA`}ca<̰d$@ K@ó./AC"&Fvn\)5x7?EQ|4<:A㱏lD*N,Ფ\zbMfX v7ߊ<gūO9<0tU\#R ]9^ǧ+ޮ`孡GG'>'_'g@p29?|~>fW5ܡJj٩*B8T]rHb7"),;2/z_@ (348sɶ$tx "ͺLz>nJeXȧutkUx{|~ZujUKx;{/ W2/lϧpM(8Eu8{ɻH`2<Wj=kw6* kѡjSVude$ǥ%JbC(Yr6~Q6`0 zRZL4!9t FK?xeHD[5Aw(DCZBBB ȧhH;s !mPuoFFS |Od|=V֨?D u 4*NPBԝo餩MWBfzoPn3.8mP\< f2<0I6xuN~^tW0/:yw3Z<>d_Ee ҔKV75SZ 9ƘhSQƮHEϺ(ua " ٴ`2_`0ph00}q7 A ^EЇaE:Jq%^?9`N@ 0oQHid,)B}(4`%0|X}Ȩ*Bf#Gv)v oon1C1$3Ċ C9K` I ]`09bqP C1pUT@ 1UX%UBa]7rn+? JM-,w)y ƥ{>O s[ Pqɹ'ԑ18yyprXy'.8aR(EdwӃn Ei@M[ڕ-YFH0Y @{~&0Xqt98Qw!Q TNWÃuX4VcV3\Y][8x~k뢫P.'gW,? pMcf7yJ<0HZJs骒(\$'nO4 ,nggUľaG1> ϞK uӗ4 >s\ms a/d\eOhֿiw?ثֿ%~maN)7TDF ?Cճ/@! qsmޜv 鸊Ac_9}k`AtaYLBXBy( X%,Ȱ\<(M]ߋǩ0ܙ,APiīIRg8FEgE+5X#z38M4j ZWiO*:Q60_@"eTo{3/x3tπ_e n ɻVzB:p}XŰ홏5Tnuf_,u_dd2g|ΒVM+iv-Mn~-@GH"M_WPc4TΈĆ58F|/0#WOP!+!J<Lo #Ve+ V02.08v{+x=Dm4LW$PE7MvPu{,Gwg\g ҙ2+2\R;wvSZO0re-Hm*OKg,&o'쌘os= ѐ`$"6/u9R8HsAٜRM <Ǵ*"TSúSOiG?"$']}'g2[7l j3!2@-pXћ< Y`q3ɼ Ve)I (%DYK`(ŭNQk3~BhoeRTht#Q#̝D!=vL==QJ+Jʮ-!*gYP-!ī~RFލ~ңF5mR+z]GΗD=RJrYsaD8wHYJM٬ߙ2\QߕYr_y8rJrR} ^F% U+E@g`|Lx(NCЩs?>pD>b9iJ(p!@ifrH{{wɱngzjNR}QI#]2͒MQ46),^a fZǘTσN]o;b/S)r"qEi]iEi`wW9w jͮdZٓDZ2B {ZQ/t-9% ]'۴ pܬ oa`a`O͹$.^HL_aoI!l#GLI!1d26Խ]wfQZQN3Z)\@ȖS]OnZ'V}%\3(x[Iﳣe^'όRLh&8v6{а~ɳ\=`FmReqVHQ<}p9 {?r37UYN}㽽&%~0` >O^\-or2Nx& Ai@٬iCf0ȤJ:qNgXT%pq [@tjNˆ13hk7lPYPgaCoal!JU c".5^ѹ.^l>y!aհ. )wX? T9GlNЭ+Mh15Z8p(`×rRAۄxx }?H雟JP FMBet _v ؅^m`gyonBxӸbaAݱo(lWyƫu l][}(Pʧ3I:*:!hU*UR@tMl4w&HRQC="+ & P輀 ʧDn d|U7 QlBךc%<1h3oIsoj v?I) =ŬVQ%AֵǍ3Qoswh\!7A&eV능X+EBsfbWC,ьD]jchmӀna\+2`ĈvfM+[2TO =m/L>>6@!@H'¤+Hn"Go6 )q82q2̽S"!-([)$8)[i åQB (8)u_$0q(GkUΰ "4*#'X,@UGUi1Uq_.9WEZ;(esZl l6l!}e(.m\gVX |8P/BMm=[?NRc/M-nK@Jf:>~V"Ć!΂=CenEcQѓ#xHQRuu2,D}%*g!m7Qe'2aX-M[97fUjTjLU,|tWN.PH*$# +KL0u{SU|(Nca{?e-ɷ+'bLɲÇ:[b' s)d^ZTV)0޺ zR'V I=bBd7 98'laÛ2N;33YM;6IҢ&f҆7V)ʷ1[(4]9UyTCT0o,i-?GZXLհrQ`xKTm΃W`fW8T,\fu,l3aUy44I"`diz\QK`{Id(Ą(kup]4A$\7Evq}g֣u[U#}3QW4!ig;wV;_ō.;Y;gD{c")恷DWf ǁx0_o0ynŵ'0,`bwH/[aߒQ3r"OQ5(YM'3I`%d.HJ<xɒk\FJ+ q}[0\ LAr`ڸX[ \UH:W߉gOQ8UY Jf _\0y<1_]MT=S}rNd6-[8GN ra5%7QTVa %zvOE ɚ@%zH,Y=[yC-PD+Q@q.SqʫE,'.) 7 m\mrrݕ;Q 2Z5d#D% PJx~M}զꊙ./ 3ZYm4 l&QT%G ٶˬ{eX+*-QEdS8`: ?dg~jԨR 6z`,|S>$N10WnSMF;[sߩŪ~2>5i4FswogWK#g@Sd(dwF&±650\U^q -`@y<;~.jxZ."_v ۢQft}>p7L6I>">=> ^{3#yIV!F Acc6y2.{''풤S\w ȶ*32(vpAw%Ձ Xo Uh_v/Y[7,Ԫu/{ꩢ98:C$O_bX%6:EG]*eMQU_u0/.<2*ËC(JBk~!J*cb |@?s(OI|BZ0z 1@/ecx&K>r /)H|q+srU?5f?lŗ(O> e+؈ap+ѼÀZ@iUBK al EYaa!r[bJ(6~J8D$RYyi3"PQu{hyT2 eQ&@Ou[8U|zLfPPm &@vS (uH-PjdEI oE 3Z& GS7[-Z_|0@u2l X.js: !v=-2|y *좯b4M>xQ +(ԏDɔFFa$Z%I[ 9 -bn$7$5)HT FX_BEn8kx2e|BkFhN%~=g@y U?bsm-;?@IJ;fB"y0"$=<+R<g}~sZ#!#boa8$**)?|Na$C">s8:n]`}d[<e,`Վ/A'55,FK 1ЬS1Vw«?JT "Q$na85;v l<-sPl|Ow{3h۬/S{'@zU\RfQV[,ШnNjq8-@1v 97@+L3#b1vn\ƒ,#3NHsv*AϘGu  Q*7Tb1 ȗH8ke.n8U4kPG}j_&WchB52 Ze /n3]m>NMhY)P1㡂 ƴ7DM,ik; Vi4)~D}sB[I@c5JS&T¥@JOA}J S/65OHMb"{|iҔzlZLTKF3H$>{, ,JI|_UWC{W5?vJo9R G ɸ|U3, ւJohǗX_|ϮR S=pyhwmp(#m}5RHqLv `kٞA@5@ŐMxecEJŗKR#O\*SػdPǐ0ҫyPPT1ǵ7[7njފg{SN (YRd3?dgۡ ty_"Gf6̃%j{FCa*PeyBN@Y.ow/N!rȘ^`+E>qc۷>{p:mH'*xJEN e4WZD"zRzE PYL#~·M%㴻0y +C3j>5{Oi8rFdF `vh"E2b`n) lBT"SZ >m%9&tgIj9,ьUIBYe( Y62rr9L[9(3`6 uypb67sc>ډ?Y2HK櫗b쫜& /ޖ)H{WQa }/},m 7㪗sJ-RtA󪫰8 8鲢Y~%~0H$N b`oWbUgVJ\[1[|%$#w[ľE*oFB D<ʁI;mgBEuA {K󓆗ٙAwm|>O3$PB&y/Wgu[V7wR +asterisk-testsuite-0.0.0+svn.5781/asttest/lib/lua/luasocket-2.0.2.tar.gz000066400000000000000000003413631242304700500253730ustar00rootroot00000000000000G?Ok66mvןw6o, $(¸?'&h{ >ml,3GM|" I8,S10`f(s_Ei\B?;=2y?Ucf C&DxU>-J?c( 2P |zxspm"oi-=vmM[E`즓WLQ.w;nP=7i4P\eOǚD )`QQQ(K`K{w?}>}sߵT֦Gq^jŶSt3H5]sZHd4o@({DL5!VzsLyu+~ZaO+LU KL~@4qD#$mU'H=^9( @% >iz^f8+4Woo;']死/n8h@,#?OESX4aw60RgϬ};,XMzg|go{ǻ7Ã]6[뻭^lxkVk;V*j.//u/GI fU< 9x_p9'낃QEdnnSQZGwz깼L󰘞ר}Nn#`9< '!NTb[v:hYE.[Xc)+ƹv]O2 6%T arOlxl#M\..DEE_(+d+t޶ Gm|AO qJ4qNƊ$2j6!]tJni,uZ~bH_4iR/zɱJ${dEoWgK!@t{?O0LՋe>d [Kg< `jV}kɬyJuM"0aEÚ]Xb߁=$HO"eBG**DlI݂a')ON0O'Hlƈ fn.p Sg慢+~~NEG^'iM݊z~k3 xh(kaԃiȢ'[e+:6H1L]{HёTPt{R jYF_a{г‰z&{ UA+ў/@Kϝ~'ą7|7M{U}Npmդ YzNGˣ%`X4R7.dB@P a pbp jQSr`WF=!x_Ў2Ӽ> 5H@ RTP  Ol,=J _D7R-`('iGTeQ5i$s|5i~q.b82LS./:=.s9>p$ ax ?@W2q)v\tVgsk3XR/5NvC?;2>EPe1t6=c>8JLk/xA?l @JO? ]4HU}!DYg.FcpR&ul {/Kuy_yZƆiXrZIc3-J7'4v7d'i" /7 H'òd+Z0k@㜼pHBVn%" |t).FϜk"$/[:3Azyca0&/X=n >HkҧrDI9SnȞn Tr K_Ճ i4p맠A!I0R&Nd|1JӐYx\@O%3wG-fN2c$) TvmcH0D">@)[H Y*3 Iʀ䕏(9 ب I}d_wD@L.UF?9T'57W\ᮻ;&EuvrXg}#_)Tbٯh` {AeĂE!ϱzy^|« Ѕ'JH?xvC#I?'" G'bDZUL^6 /cOEi֧/cTz2vNOp[:ùe!=kIfN0\3K]:XX{2)SЄ3W[& ˸q@lX)>BQ[ :T-~6S'TJ G5&sFa@5 &L9M[c[I.:iJzY4(xԎԖ)ԥ7 Yum 7+]x9Æ@B;+.kZ&/(UMUHDj<"V Ȋ5nT]-AATׇk`3ąZ)W)I۠ v(yO"=d.Mii$ 0&82 kcHo4A)Ku Bps\y~]Et Q5ȓ^9C?Hacg^2>b:ʉC!C*< XZmMŐtin«.>5R`Ia7qM04Ą8Wĕ`+!7: V:uJ&eq"'PfS'.&0H *]_Qk}"g'\*Pp̩|į#ߡD|> ۋ&%u~>~}G Ń |S^s20`Sϰ7qO j01^a$xݬ_e6w(tC:s:7OZI-Y([(_^h) `"BQLp K X~ y)2 _E$}@k데)nˡebʹN`{EI(1KK%PuLs.B$)] c͖:2+ČB_ԡzTK-Ha/5oƙэzhlJ=RG5nWWKzQ +* Ƨťen 軴muAt? pbn`(_4oVk+ܺ>-][Kృ4|㚱aۖS;L bS}CSU#򓄃9`BI$]VKX=LQ9wuftjϦ*RN16~ρZ5FhDe`t#6B7@-QQpxdè*,~J5VA,?hJUߤ48<_Z*T߆92jy+נ{6w@m2ҍcyIbc* ϫ*_) ]8!ǽLe;VɄf(/Mfd[V+FږۨMw;¹sWwr aq/^zǃ}16W;e]Џ Y (f1OUj@= 5:X_k#:ʷѥGku²߇Wy3m2v6i@DRc9&Sc!& ڪjeuE=z R9vwr(r޸nQ7 . 6|;v++V[(5Y8`t Zs^tߑHOC`纍 vւVAH8:P2QL%qf,l"AzƵbYPj,  0+3 bLXoԷk#]S% P.DM^RnblVm F{srFuVaMl'6!~:walڽŗN;է9§EXᬐit3 ,iI^@rEdq{*ΓR+cJpQdA `&:L.@  1ᖄCL'B8k=uVEAp>VE"+( !_U$Sw֫WǯI1 G+92t\ra|DŲ8L4Y:3i+ Ptdca k%Dزd\DŽ 4&3;3Cg w *Lpf9{Ƥʲ/;J8 K '3rVPL7w88٪4X2?-ԮK*=d9̄؃ ;ô H$ EBqܝBh jey|PJژl[]f$A. &$Uk[B7A:S01^SxX+՗3:-yFF"OH&޳FF q< +la=GwZDl],/uu$CDEQ%G]2d%'JWJBгTxI`3gZɶ$Eu,KɈ9S36CGE$"5̮ۯB'3|2eE'ցd1ӧh14Iσ_눟PU~# dg5@_S l?C?cy*ЪB<.  &!UGuC:FXZ*%+2m.D.}#!AR/Pɀųz\k͗0T2 9˛ceXN$ZNJ0r2P@;˶ 8L5sotkbɦH I F {X[Dɲ)۱Wi6d${t3@ @?!>3}Ԍh6#ڬs ˩6C.W>| 8β+f K`530q턁K48+y2ApUi `A Sy9}R-uFQPI^ ~}(n*UL!dWfU6gSzD~p+Fބ[UUA$'Dt@R0ԢVttY-+0t^n=hO4wd6_4 #j (O&;gIaf<<\P*m0iwlA0Ӕ=c ;>d,L/u82|wL%}E-֋|4%G %=![qm  TnM(w6dTOS $cԥ#f*XzQ)ղ=SWlb@ [qWZN 083;O\FFB;V<i~}{3#Eԭ:!L& I^FdvX4-8+Y9zX enu9bbi@*$FZlh1rϢ6ybP8d*KfFr!N<`0esHP|̠hymZ]k r.`9$u,tP|}IsLmbx=;aY=Sh0'_\T"?P˯MWh) *l uX{dmn$K&ș=<]1*IP("qbzigMZUFBZt w~0L`?FR(#J,ؗ-HdX@v6qnW0,S.K]T lB-7Wh,3l@%8> '[+ڮ`O}26/UxrIKq}+c+I ͥX."$QLd jLs{ s% >ѲqwSj7E8QIbXqW#wcJ|[D+ Z 2\b&^PBa0V}'0ځ4*9@*WF@R>CΫPA&0$COizPD=꼟#pY$30c%6 d& ڡ`ǤJ^7mĨI ⾳&|l0x $ v_-)+j@)aM*HB߂TgSVRVB[:}WΏFz=F@Dn6 ZF4E"@+ / {E+D~CHP|r!k*r2^NlQSeo 'H ('lh `jا|%42j#3OHc1Ig;hcd{@h>Ap3~ %iT@< Z3 )"Ϡ|_D` "f5/(eF/0 )bvMju*/ ԅ}Ӯ;~487]V[מ?g-2q-?ԝ%y}琊L?#\o R\s'HHQVLYs+KЪFR~dBa.ra_D2 ˵M 3dq(<0} 2+/rPeXfyUVT\{r88xY+\&Bx̚ Y-qZMi5|UVwhwK@|çM^ h} $ $D}!]TL p1ʊMT̈Rz&?pU}BўPFsP6P~1D A\ayN"y2  Gr4أ_^E%;D"LG_ӓ,rX A.X`z\H 1 xhM$0b?_T$U`8Q2a_C^Q&"n`vl@'X|"O$xQzN1$\W{Vu"1}qj׊Sbv"9jt[Зܧ#AqY3jd=a*ZX٩Rc/ ӋyBjTz1"YpJ!5u! gʲ±sNRLk`Go-&*9vڷw/ov MtER9 \8Y!GxB'֘Ds~fZj ŭXwJ N _9<(?|ߨ<>jfgf33UT7Ιϙ'&?=RD+y?qEH"ʼnC?-YNc$|f -yZ9,̌A'fQ(z)ͥ%=Q1ζ;vkx^$_ WۜivҴTm_.-.0J7SB-V)bJtqq0U 4AJl-*0Cq"6!HU Ŀ9OS ńfV 9r(#QCQvfhZXA7I3@yDO9\!:T^rI-Mk*>Z4TYt~a7){#l\XI։3 @k77,` PKDʑ'\C(\lYƝ)Kx9QMS*g>\99,6;fEm8#K!ail+61+M7w1M .>$ S bD/#V [ID?Q@v r3B žiST [B-'"OxPQBP~ifu)R&J0a6msZJHop.CP+O9Q) |J[Ƈ-2hIp|NqSG(y}'@٭fR~8,qTɛ0_^4X^ź%d1E6a4Qvzz~5̢Mym=zhP2+f!ڸlp]x b*3` S>l :ƔB!4Cđ @VtDJvz0S)!YblA3"z0 #>]^)ק%Ơb3(o˨ru,lE":ȣ!mؓfX:< t깇AEl3爨E :葎lacT¨b`xHP"Fr J5 3b)R.j*k0_YM/Tr?oknIY-sIVKZƞ"äSG=94tSBK6MUIl 筦RU2tf +i"8@`RЄR }]z-GX\qFX0! LaVF6UJ߯+pύ s0lsq资Tef;<&ҡ {\mosUspןMI\_'Q5ኈs:Ț) EIkAI,n , dqp'aՐhqa ZP5ݭz), #dZ1sF  $82_sŒ]K&D%cžrˆ+ {%z*Hdt}#<&b%x…1 :`ƙ,75K\MY)ܵ&,b)1U_`JHZg 2W =QY8| ^ȐP|#`M=C0.3\*DdwuK,ənI/hJXNZ!hE4s/{ϻp(. mB%4gҨLe=w .V֫'((^WcupU'oZh 'jFgT1ЪfOeUbO{ٜO XvQՒ K2 vSӺA LjuA)66XF#vs&H2\PD?ZkCKjIhbwѹvlky NYGe|яjSyU+9w9W/H~f9'mfV?Y?gsf۩$Ř tH}"[ @۹s??T(њ7*2(c<._SEgR,#ֈaCVTIǟw3ƒKB'\YpX)s'$e";ܪ hkAGU]A(+#rs6YrEVUsN&F!*sIu|L-&JV)_ҙ^0zNQ @Kss;VsHiNn?Y&}&K4ՋʚK[E#ndT~ ԣ^h0x8lYJ|i+ bHϱj"s*3,+()_K]UurS- FR)hwRET@ ƍ@{'"_[獾n<``? ޳3303e'm0pK&:bژp'"Q)\ӵ՜PC5|hXWxGzH+ӚJg+t'֧J ֽ(GUa%fTE1FDO|{*ld!p-8LE+Cp'惟HJbNcRJ$Ź`(.q5,nio5/t!.T䄨_$e 7L.V!nHA4&3(a! @y2ܘ5(rɅ@*SeP?ƙ[:gߨ:5OMt3=Aw{q .BF-UU5DpWIe7~MFP* Naǫ¬XM A"Зh:y@V@ٮ>`R\m]|/&rʊ.0;81 OVSyNO)8Tm\ WHRq?!=ZxБ}q$̏0 mBCTKNda$!شs27t&u"w3?j8t.KF^0>mL_!8(SQfqq8Ȧ4+cn _)cPq2ZqD-؉l aWTm'<-ozq*_P^t[%q@C3/>-ˋ]Y^$D€7i|NVӘә>Ml? <Ί)1PZHʈg C5(q(z^Gmv;}h.~g\Q-0gV"`_!}"sp?"M98me>Nnwmf4߿ϝ}zH Z߉_BmCͱZ疌 Uᒮ,[ .{) 1'RwTnu oY>k_V~֩tVo>ղ] -+gdž;ˢvl/ܥ;ex޶d\ew]q_l淴iսu*?r̟js[/~G_)8鸭 F/yrڸD ILYՆlm.mb/?֫|cC?eG߬Y-Nٺ_ptY-m`{7?gf.٪ ;y3g'w,?_~9q^N^gKvLjn=~~IkK1}wv.ۦMN?.^xMfUV 7^ܖoV^=/^i_uIZ+KFMjܷ}46}M]3tEʋ}ۇ=8ox-4_h8yРw'{εi7.O9 |sxg54y׮]|୕+W^?;}?/,iop2꫻5NtZ#G-yХw nƴ  t l mum esڄ+w=Ҵۥ]nL۠ uuȐ9-HYӏ>jڬYׄQzs_v칭^t=?6nL:c̬W6ĥgmF|^k}zϧӮ筹¶9cvoݑf?*S=3K\旑'g-;'h7x vEZ{VXw]ذaZ>ȦFo%ΟV:7'W3ߵn׿-Z5y٫7_wuAӿ4%1dvNUG]zkɺ#=Bݶ>i99K/묦J:JXu,cS)8[{7o-*^ᨓ emױ;U}_f]|0z٢_~ʜ8}ݖn:׺ߔ)Ue r5{^?BG~:CMW];^ގ/&)=6*y䜜'ܷg3.cǔC\mᅪs8H07g$zbj7zԌwVټ|73COY8w[1z_[{'^O,RUO]Qd[jڿ]3!E=zңMm˖w_pۘ/R'^pwIJح^[c#Ct񏫶|#o~"m3J{_k 2dIII 4xmۧ%?<ەw^bۄ/^6ᄌ5oz>nnv.\u2ϻ:'oM ޘ8pۭo٠a}S]?Gm UN]xˎ?{t{NԪٳ,`EO=֫մMŝzpuFL?8k{JW& sтk>zf)ME.lq׻rlO{-nז]x*;#^(=}YoQB{|cj%q\yGMl{lB[滑aOm}eϓ=7o.o>i]m?},z<Өs_p}syZgfJwMsfjOꍍV0;cN PheolGj4{~zěS}KeNh`P߇_OU!2W~nwӍkzȻkmy<%5Hjjky۞73ׯwζK$Ou~k&m~BV5wFumc쳎gMbI)[٧5-RZD#MT,53I![ʒ%KH%[%ZBҢ m"kH9-r{0{^9z+mɗOOWž(.E V_fe=&Gj(^?wQEÇ,n:Lϳz9´|\ѭN}V0>nlRX㞂?~g 쫒{V@ߋ7pC۵:1٨Ĩqqʕ\tqrڴ}WfDq7pvu7mSÇ_m&=)>i$@n̯Tݲ5feo}ݻg$Ox¶NA]T1ro>[7}roe ERGBfOPHO{‚/4hFC߸Iϟ$[ nу$'-WUuzij1mǛ+l4+V'Ջ[S1s/ <9uʍ㸵߿IwitcNw_Je~+4͍כ/]t2fu†kn tNz5ܼr]cgLn][v$,O0NY3lP-?ͻTjm[{.Ka҉+V>h|z/nOoqD H=nk] U|3uӖan:1P0KVsPEl`h#/(7zT5c)I?:N%vut#A/J.vZb>9MiO٤i۷oWO^Sl@8MަPksMvqxiº^}Rҙ!53֏t $i2+Z?^]UЬ#f{4^KXC7Oju)D߇^[n0ܜ>1`mn;JG/?R}||,Yٷ+*{ƍ2*z>'2+#̨dWʊ4xac 2/RP5f%ȍP߻:шAHP%]rFfP.$36RZ^+KKˍ4pIr3)*Qˢ ٗ4R6<M0Lؿ? "!hbu7߿N5kd3=quu=ryan22'fM8$ ܴ$ӳϥKOinμ;hyI^)O{\[Ȫu?SWWD={8-F>62iF،'_41픐:O˥^)WK(vxtLo:~;J\{S:gMPWxxNXo#GXcaݵ]SA: }hFةSi}V^D+B{Ba~j<ĖV Y0ό~+/nE 7;Q[q݊͟40RI$`hOt9(M N8S7Z6න!u?gl-8(/4S",?0OvHaр{Ĕ"&-P*33 .}uw$mywݵʕzYuo';:+s%w[q{"UԴe"@ݻvX wƀGk"׾fC?kd0Ψlx[#8 5ۑfm%cB g'k~yBCzSqlx+ ZRϷra!o.W2_V**`_2t~,# Ego&[kP ,?( fŊݤ)i)99e&`>5zHcJon<etĭ[ إG7qq) d)"sEX`…3sd?QM󢯜JI+w7o8,yӆ;ୢbSsqjXOCVn /x1~0ԟ_f)SR%l^\-4zQޡ3g2g1< @.ߓf |f`bTxmL]lle߃KhsQѪuޜ}XN<+|ҳ#4nZKQF/ln˖F/ͰH ixZ٦#&oTݕLHx:N?ub S-{Mdy\F ͻ\(%ٷGUM G*TGs&4[ztty)TgqQt}牽nktҽ}],CcY+^3yJ靹:de[. J?Z}C!=.>i}d>=/UMLy3tNk*goF jƫ&Яv|! + F aٱ'eU_LJbVm1xűuj WPVmx:)W{L:p\eAK"u]LYnƘ /0qqit)t։)yv7/oCttg͛7xsP[IիVm8̴h!>3?tq "ZzI, Դ\w'99'h_s0mb!?Pl9Gp~gmvS̉ojo߳g_6[.VG/\P6)zkbXtS_Jv{5m4!5ڦ"j*!ݝte!l7 #!zGӍTU}^\}E*WP,?SllKmvw SM=3R8u܁633Gx{y܉HԆ-{׾S`sce-rb|ǢkUe{.VajH }jm!W&O>{TRWC.|G>z3;bT:]tK\m)]*ѻOնhkuлJ7{k]Y#?4'7wlA ^=~۶&>4q!g+\QW$t˪U/z; :+E{xÃDi^~rvV O#s3l{DgG J,2~~~[1ÆqN%;YD57ҵlr3Fp&X;{>vr׿:яiR,a2/* /_d^!$eƂ5UU'ϵsnk5ds/.xqU1]om:_?z ں{wxo|+;Ijcs땽1wUMnvz2o=}ZSқƌEu#WITSss O1 zf — T$L^i٣(}>Ej:t옦1ޖ:Ĉ=GZн1FΝ;-uH…ae{*`E~IaIɒҐσ\:ޢl4K沈2C}{',o(7ɵwc.\zK∯-K+|6->̘}ҲQ{L+Bᅳ2b'OW!Tu^[jr11i:i-py9&a y.a-8qWCzRv=nѷMaٔeD ^LsV9Fgk~d4{TEN9 pӑ1+2W|VRGyĬ&y7oVV`h•y?w}ۼbeL:WCtF13i3Xt⧧Fn0ٻ'J9;|t|:vpN E˅{?ܤ>9}޺(-7zB.$33stw[[+hp$58&7'( fZ;c,qJl<Ѥlxp녁߿l\'kоu7^cT?TPY ^F23 ʎpiφ9Jv훗ӧ^ʻRH [mSohW[{VWw@3S̓'c*&P7taugNOL@m:c1]?_Rk5vQ3f V1'3nː -~8+}"޲q#´2lo"_FSDXgŧN uqk}g}"?H1ɹgk.(T}4cڴi6|/|bv%nyZ2鼩wG_5}4uUBYrxFssn_liiz4lK5dV=͈cVn"qx^('gKƊlPmz[7XՌn#䜋2~q)kw?Ւ|߬,A-Auusۺm-JZ82˦T-"?4ãWpd}Puf=,?-P _nMXl\JY4uKSwt|Ziݺu]t:͗X8jIMyv@_Y> pJ{l$0n6+Q/ͽljNvq\cnb CoۖwoPj~ܰϯ{ޚ>$MRӋ<}zRCKdDb~Y tčy*]_*ͣM5~9"'q7?~Yss=&2Q?v5_%9ڪϵ}iKT}fx7HK}.^;gu[GE?z4~Tb /h _OJrިuǵ#(@8jV?J뜬h]u-ja?^x#ϹC0&.R&ą__Ѥ2NJ\(8KzFV'580ydW=GǚϦuwze-~5z0y5&E淬aI=V铙k]N:djik߿{wb,nC1ɌwU-+  ="7h{dzUˊR%6Δ'%委BBp-eO9rR]*\n1U{@0Y{`)G 1aK Z:la IݗNJ^n;kS+# Mo}tK ;8#FyA9Ay'QᏒ x$>z3v;¬cG4|"B.H KBt??)k$K vpd̞e!ֹ(FY@BE& bc,_-JNrqHwE:HgZCrJ4`ȶhsd"\l&7nXU?F_PƠuV*C #d}'94.651E 8Y p;!?V, ;MxL3ށxA!bbaxg.q: |e9<\2X?t-"xr(|FmѵJdx`r1x7C{9V?7XX"~`ҵ,,O'"5QhbG-5Hz[2 (iCh(@\"x(HN@tGSB8'\*eUP.a`ɨtAYQ.Q)>l($1PBgZI-. `XlPE/amB"zb~ "g0%B|TD w}A@Q"qUŴ\ʣ*J(,sԦ: Gll4D``8Q|*Q2M7kSii:5ԉ0wC4 cpmr݅ u_.jA⩇`\>x2s$7ajn5&ct~L˩N&bCp3`!+b-I3ƇGzg(@o\̓.bmS - j;2LBiqi^a a1-4xx9~I L ΂!A͐HAWaj@6{XZ 4D"s&b#^#O%~,UDeG,R֠rFɇ'R;t^) PO9S:ذ4CcTJ Mp(TMM?*F>> 0Xm:L8&@r'bILemxkMhpŐ;  `8WxDXHmp"&lW~@$p\vMRT;x(2,2 7DAC /s Aݭ -(bh5Ie#<}|yspIX+|c 2B-.4}!wc T.n 91H_ Kt*cp`pq}oߡ>Å \Ω4jVVe~,\'r`(%&w:3&˗AE1 'C\Y+bȂH55!nFӄEr₁wٕMrw u \` A$qڕޠGdUX׾15kv,(*&T4((H B.obyI S@"NVzPoց2)Ubjoj=TRC8RA혔3Ť\FByſ,B&>cW%] YtVQ ;qDOwCK_aءNl,-5H4e<'zZ~&jDRARXUM!Z\ÉDF /-,*b-X]MR?(6@E ~3@j؁xOe?VomuփO  `sHjP|@жF7C#>v>䨁Thk_&[bHHgiI`rbm\FBQ"@m@ P#kZ#aYE_(,:D<_@,-try0@A ID-{1^@li|}!u8+m3xzM[9tu'ӓa '7pQC0PU騸& ?R. |am8ha!SN 3s|K{SsEu;ݢl8NV];b?N8?IvOLg@- :Hs{Nk)( CQw?{֛a7m bt_࿏כ }/waiMt G7_c͚Ë6|UVy*oÐa<m&!$[c(jBд@'j4.ðCBw#5N gc(l =E1ȚO lp W˥] :Hai,TXqR!F(:+ ƒ)C.|OCѠ6a3@'?i !ԃ#6cPcwԧ$7a'Ǧ@OW|p |UUAxtBw`' ivF, H.6pG& 쑤iRN(8\ t,!⧅Đ1ɔDPCß( o+bkI>jUt틱j~Ir|=I&=CҏT XcӹD8 U? H`U~H !dq.ă,C"" >trC6_m/Aǭ *g,9a HOqEl:(fE_nPXP @猈|SR F|+ZZh[)Ʉnƿh4)תޯ kUh\Gdr;ZUVʄUmx`m`q$I7Hq|(8:<-8-Au ou8v_rT,~!hE+{'>@8(&?OD% ń %œI.0O>r%`riÓoDEt6$"uF;3ntnaɗh脝?F9@^Ŝ]{#` Ytva"Qqt5>ˈBNۑ )`FYb@Ff<?Oh%"<b_ȼb"hK9@$L\U3BXq͊_kz6B{lWk:Ͼ3BCtYp6̽y@mU$lCUpZL+bi+x\<<j/ RyP( f$;ō" ȁ?ml3),n6 Ԯ[ 28 5'rW4 c"`Mx !G0;' V'(;{?= ht=pﱇ'*! >: :(`䱣`K?>[xW խ;Q  a>[<+ hHv'{ I$vA ut )uv#ť4DF7L,4A#d%WpH%, j*Л RSWxI+Lw![Ѯ "ṩcBI C5Dob>2~CI+K?Ţi(ج/ #1\ p Ј/YS/tW `76젉}LſB$Ml X}@#bT_5=4$ Mj\J=Kf ÐDPQ- Mq%>wƑ+05o!Zjc`fX|$2)cm `TusseM;JCCcl= ˢlQ0?aEĭхӊ"lBl)@,d(PI"8ۘ!Ոpϟ;?y vb 6$2ᔄ $)?aˠƶ<Byn&Cfzp;u^{]>NrA^DW$6X0ݣq >h(8NG7'TFd#3qs9i0rl}SƢzTF'/)4taɈjSHGGT3{aͥ4sк2'Ի2"5Ys2" p, D9MG3PU:jtsS>z_ܞAZ|:H3i_S֋Ӎ޺3sP?+ / i.n[n1NHE GwtSRlEU\nm@EH55oȝ+xO6(ZD!x*tz_;D ;KFWr0Կ 1mPy._4WhZ' w+l:~y%AKxH຿/T 2h.// No BМ?أ 1S%_`Y# NRy(YQ{PR 5$)!LNKZ:& L#C6TDx amz%VS%[|sNs"!24 MW%63ͦ'Uռ*\o҂l٧31I1%;D-Xd|zjq~,[A:K~KTgeʔoQE]èc! K|: DaAJW#ltlL xvvV+0[Lhs"2e~W@X߶J 1uYeL)6" Z"Տ>0*6Rޣ}? !(&ܱmnD8 ;4`S{Tu{q,%3q_9ܟ9е :a*]Wjych7W)MhX̧ivJnқ~46ZUq%xO ĤJ/vd :i@(,o1l.i;&ÇiYgnOfͩogzVO@CnOepE} }ӶVvON~:mXAr mSM?(yBBݎ+a}F>^e}-dhM{40\#qZr UO )4(XpYY.l|Tb0h +hBz}΁& %)d-I,v+ATbE*2g Z %ަxr[7& =֊Fĭ Q-nQU<>[_Huw_q;QU7Wgj͠WJVU˧V'乷*ۉku(OBR,79/D"JԚ~BɅ~žӗzv᝻ּ|JMИ1(~V>e_Eղ/`[,&NuVΤq01seċ|B]W%\bPH/`5g)Rͬ"5:˺(M6W~6Q/x$mQ̝CDϝv&ϏI3LfBӝR,D5*jy-DXQXO)܂&52j ?){'A%lB8%:ㄠx>={P.aKk ե0u vptͦ'Xc&t:!Pph= &㚎\L 2c^]&8&s ,Z$LLڦNmEWV4ޜO}S_h/iQޠ-tto؎z{̿ށkZ"&(1V.B@tH52hX{ULC;v{rvm1{4fŒ1U"3HQ8@D:}ͬYe']Z %!J }!Ӝv/B.L/,sKZJ[Y1J:lz5᭍0lBZ^e#1Bf4d3 "$F1ނ W"8qøULuOQ$Փ&e"*UR<>lT|%֕KS+, 4Fs&2p1vj*鮐Oe|&l_( | vEQpd/3̤Nk%Q(3+?A\0I?2ŅH,s9hFQ\F81Nxo XR P2gSTQ@x( Oz(&QJV'ՇvX43lK,qaG^>(!xhzci+ : DĔ+@LoT.oZ:{h^!Fw|tN>.o燷>dq* =GGѶ3]ǨپvG8 hᨇ{wOd8 ~o<"a2Iyx&&T?PA" f9;nf%\8w˳g*r\T2] pBلIbTܺj(xg#+EI\4@i'zorgbI|bV%j"VgrS`o/l\Ë8BF_Ec+_u(=eLlѺz4k kQ8Yx5 IpmA.*Y0-oSma >KC)TӔ(r\&-I0rJ< ImYrû:arv(:E'ºE|a-PؕZ! h%ࣄ&L:T@*yagJqAG!Iג12>lmSa4 _}mdqZv7&]?A_ٸ]spV@ KǧBOw%,kBA׊s[Jϡ0j5%ivÝnqN R8Ψ߿v Y)js ^+ލ hM+ F#De1q@hUWP[Eں_{~W6~Q§ش mJ5 >Hr/Gl+;JUXM+$f *lZQ"u ԃnrԴ :zT8I γyw ׾&BL;G$Iج}Ɵ]-rGmgWRV8.`cW*,`TFK7:r2^d@hvaT%ngj3e<5lImdIdyVu8J#AǢQIt fF~ a82sQ[?3%xR3}\1I53IOcU Axt MxazCyՊW*i[8_"vU(ѺO#Iy3RvNr^/PJvF|yyH@U<ֵLHW|cy/!|<>}Lw$){O2 ,W[>La& ^[dN~֓M+HT=ΆDxj1aJIe Q/9LIy1 iC'DžGIg,݃]w⮱z)Py1ch=oddЎA&;8}߹ܹpCwNҮǤU}X=.A𢆻6oqK⤯|󃜭=f֘M3M+U.JlOU8TI,Tޱ=ݽ*7jCX镝LA$"23$w3Ǐ1Fro\-Ee?%P^TުR-5A6@]Y@5ITԮ(VVˮ|* &dW Mv/JQ?5 m1%}+&2ԩX$k]3q%SeeEfo?~0 tl@ vӚq;Hv`WۻGۯ&3I)u].;8E+KEا4BT*9!=(V ӣ./kG[Fy4!v0st;K]E2','Dc"Q֛c XǓ ?60>äϓr$cYlzA*n+oOFY* ',B`s55̂ NG nqT4 M9j< ,BAHs+)x>ټt\s+GUE`=1H`-a;/q rb/t* {.Áwyt8ms*N VrTi[<;$6r|q`cN_E3 >d|Ϻ(͟ 'M1!L؁ࢲq!nkS^.tvw<]jo,o|Ah]lSqKkXKDj`T\j`,Mm ͈7VQpOG6`.gur=K(Dsr^\48Au"{?Zp PwDHFW ҵoϺz@u^֭ŤzQ&+{b=mL402ePxq (wЁ+KΊ},YG-omp$ {GL-~.NqgVSB# #H~-H.Ħ Ʈb3hHxJ\(4YLȞ ZrEAd]_P-]mHc2,Tk{%?"FZ>lN}116V'FQ U<3ǻ``+dʼnqe#B&HD RgyXGJZTCm9ɦӭ\׹u/P7,ӋH];f;x)oG S Mu۳ c|;DѧA5vX:դ#vijsOf柩NkI(t(I:}'zqY]ぁUGN6Vs:*(I2ۀ>q>hcyfA ɦvs9lrg93PK@K.@egtӦۦ„<;\T&+JyЦ0X9Wo(kiw`iFvoa=c q-bi:z=r}Il-fTi4NsM)H :qQVM+8h({t,]iGЉЅ1l`gy4zk:>(4%̌nY÷.c>V& |=ٍ g}y "z?r0mR"K8.nj^R_mҸnŝw;&<#rE/3Eԍ^~Cp]i^r |ػZB{ТHFU*ZsHe^OC+`GaŨŜ|I*9_;MϙETc)FYo#\耝 p00ӨʪRW$lgt]IʈJYʮ,R2{6Dr?*?^Ɩ*.:|X_0=YL)$s<>AD'8Uby3'm4 ;eQ1وxg ;<0 y$uHLgTb2p} {U-I52btB^BA(YVd8ed?Iܡ%SyHDUJfSUBQ+J l-f2WdLv8m%EKGnR/seN { Ʒ_yTF!ʠ\zkZO@N*MR,Fن\ìF/ zJ͉c~E ʈbO,eqjD\8[o.V Jȹ]q8)=xxTA`R>Bn5Lbx-#/U"ΉV$f+f*ĖY4YS1p# ,6i P ZËkIH$ 9X@z 0eS|WSNkXH0Iտdxȡ'v=wbݠLNrpt=5h3b΄z%nٞ"|,/ez]Kd6^؃(e@y2` ,QqY IblR>RpzdtSV,E\Ik%I " 7 M{Y{8;a/4:QʁpF^y t_ "O$-\Pl =&|?4 3PFȾ)RZSRTQQ0 sl砟s^mAUI0Tu>ۚERKcW.5{B@܎e15#kbB|*PUw\d@|nA\ޏ8h:dOn#tA ^)=(ηl-rQD9v@:-5nT7V7J:csbۺ-7/e+i1ew=2T[E"=3=M"\yTQ #6@Ǎ'W#7%uF'*"ވ&5MF_DkU2acG=Ǖȥva/BmsRS*wdk~d˙ E>z1^m隑#{ rq$Sh*4V% Ydpde>)R$01}$4s;Cr(3a/WX}Q׾e}4"L=T*JHTԦ^o32)9nQ}ػTCP!$rfK";_z;F$DTe OBʆ6*K%OɪDORy&rlzМF6SV{ܨ_&m`|.(ѣ`F,ϨsV?s?b@#׎cus.aT??3Fe'^ArP*3+.whqHИ3zK Jz>юB,h ]*JHW晪K)kXVY+N2PbYW&;PpgeVR4l9"rTwٽD8ƘTƨü JE=y-laY\kS+7,WmyrQM%îJfUdDV %q<hV;g1.eYƄŽTkjٟ}ud7wQ Է\kuȒ$P|3LX7,IŴR9$|q4DWv° ĤX4n:XONA6u?Xx2|@ULߕI%t( ~08(7k"I8fI$"!x"!EB&FB( eaaFAy2됏(ӁfLrךա";>n>1%q]kL>L321ەnb%!'ǹO_[yW{vYu6u*PGZ05.9禼z7M|Zs>ʥUy:Lxw)%FF QQ]5ĥ \ҼEh9iORo'?'7'i1LmQM97i_3rGGZ7sq āpjڽ LDԖ|UG `0(>i VC+YMtenYWb (dO=o?7Sϟ7/w}'rΓ)JzZ^yTh6>o#&,ϯhaf/uv6@6_C8Sx'zw#Yc( \VP2W6: 7UtCtqp?# IP^=fL1;mW1L#`}ht8,%6Zy>a9z/WM8EfY4@4<~2h1p !y&ji%`pI壜(JªvKz6<o{@)GwQxC_FKQMhl⃴e|> FpoHJO!>(?_Fa"E ="ִв( ǧrݠހ| 5Ъˀ'[Q̓*J@>H,0<%żׯߌ3|}_v{qΗZT-ݹTljL;?\yextLES\(Ɨ?\뒦1o~BU~- Q/{~^_p[ ~T[3M85P$Y:Kr&ݝV`i)9q9D4hWˬ v)4`#^s(7M@ڃx®W19k 9V#)sZjv*A" ;GԲ9 PP,Ze4DKUf/09 XJ]!Q0Ԏ0ۣ֜ bDLj((i R9Sրѓ"Ss2L-l %AY,_D(1oOi 3/v)yRJ*g2@"$*W1;2)}d%؀c V%s[ ]My.%,Mx e~{r{tJJMk_\3a`EVu"#(.(Kj⿱^2I &gs"L=A"iRȕ4:D+K]nX 2uaRn$`Rg*ztή{%[z8eB,֔/O#Iht)zV/brZb0(`3"KH1sL/DKeLu^g J%gOdTA$$(LM+\jGQDPC#2]t`f3jR6VUrݨ2NB~Pu,l,)5(n%x@t IgDbfT9(0pT,莁Cy7p8ݚ*2yn!~,oDcinsw3w Ӵ{;~QTO7IT t!9haR%pp$9e=qE=|Cg2걎̩,C&:\=#NjsZRO3,E.8lh86^myڬJj}l`0BWTq.!ڝ|?a^wdﳶ6e\5x{ e?el\YY˭׭|A ʲs89]_ )e=#L6]!6s;jliɛd̢ -"Jį][_rKxMu-.̀cELvxrl/ꢳ6[[bgÀ oFEl07T^RAn,JI0T{'8G*7S'%IQ؀ T/|?1906w6>%%P) B [Sy;fGIX ^omQ w6$u \B.B(Z,i$5Oo;|s%OvHպ/ SbΊ8[O٠ kSiN 4#ʪ7ʹnD_Q?G7O(2a.,~ ЀJVѸ١Pì8aAc4[\ec\$| \\;Kkpu#PC}^jE#}pa@Fc2'5Ȣ3.  as-{%17kC7\gU5P-ԇvݿ.î:#l0w+B4»6%>xl=t }v~$N0^ .[sW»;5D1e7|,ƪkRG(b(/*NYbC 'K м*ujp@~%h `:wP7pʃbB%_~(@'QO 8GUGSp}0lWŽgvEenP!|k60q"]I!GFb6N$(S 4pflL+QqUqH Qp܅ڸ5(jB^kpC10fVF ӅJd ~?d@X逬r,^Sg$p>i<53tt&+E3^Ź1˵tBAsa-tQ^-R}ʂiZEA>>F0&Ҷ 'iNml gřpfGX{v#c%sx.(1Gf@d)ߓQ\;/r]h^r2z04Қ߉5ʸCQAC.*Q9l~0k{=lȸ7Γ0̪NҰ >j-&bGRnoj9àzF΁b]U̇hXmյ<Q_߁ |mqOѪa5]*_a 4*xwϹCi ޽ٺp[/@ڪT&OT1ߓhb`g(}W_*+XKSyXi")eo-ۀ=FH%T%޻lJ kOԊ p6n { Malf@\=i@i~}S_/~YvҀ3|-\^h,B L{eT?~X ڑyQ)j=gGDdmNY4gez$p#-WXy{TaVs IVuj^^q?EqCyY k1>|('T|Vz@ 7WFB2s[jk$mE.9ڊo yV~%%.TJZM)L0@Eo R7jO-y I<#CL XnS2~RNHlhh3қi kH7u3[lIR!%&vշxf 5hIVې3 oJ=[Z3TgiڞΝb7[SZr91Eš *\H2j0L_ 6C%'j(U1Q=slOs΄ۏW5C]QYǓSf_rǑ0#R6aI|R)95ёVۡ8*CQ̠CJcaPǸx5䕭",L"=3ӻ \3OW $,m.?;m|˻ֿ4oL3/Z6ضJiYzzb~ԾX\L1`> D QxȠI "r7!X9uVnޒYt=, !(UJN(yTrb_8 u#SNM@&*!_u˽y.3ۀ,[t=Woooҡ_4yeֶXIo2<5'WHq<`v=_R M4U0u$[ uLYubr)oǟXֵJP" )Ǫ"*u yW'0{G![J1LVdcbKHi*~Xml# 00>;յ1ӖFIq怔Ycjq!oh3߽а^i72T3v,L(R 1ª ,L[( 7oc:/0Ag{ɨ@a A+su/ 91mh%<`wZ/q$EKTB-6ނ2h&K֖k4e{w@%Eu p2o,*_aJFY+yPtE.Jc4҂[zKpBy ||SjsVkt):^"Ajw׎tWG>A SKTdrilKb4ɕz &"ˀ} +ڲ6B<ϐZ`Rgdb$bũӇfDmٰhI\o)H^嬆£vI}r.(JR#[G\ lNxˀxcEI /W,(3L}8*){S0!+'_dNH,\guIwbՒyS&N&q٨Hl&Q y*aYMGMWZ4̯dZR~!o^ZsnUufmk >C-!ea ڋKP2`q`@yRl1b+fNڇ.bJQ? 鴁Ä ̠J19|$ЪسF-\60N acFav.7paˈds^;P#y(hΣ̵躯#b;HN?#؆" A]5WmQ\t~O涓 wxtWK!킌 ;x[M_moE\Fj1.QJWZܣ5573=[;Yl-mE[ji5lAf|qݒVd,$,a'm|n%+(bǂ8)>4$gǦAHYwG.]we\e2rgj)(lVXyCQ6Uu_]v 7ai^q!zکDп-쾍cGDZXiXJNoFEyۓlgETJs6Ȃ-L2/ *m(aRQ֔fҰ)CRO6SujZ?.,)ZEDϵ/FeaDѪJW²GL0z9Ҙб5@H:Tr( BBsdM6BS+@_@9+8,.2F{ į֘Ĕ4"Z HQf* Brz$U(EȋPpq抐DL "@u$t[brgNjT4F;_SFOç3{RW~bNa/h2&S03\EsLuєhV8zIrxTiճI3Mﶇ ͌Y{+36+4L E(+[Rn)ףTث2*RTs S}m;RPNQjqٶSpDx,~(P=jù@.(kc}\{g[)3v0M[%^S~32 淶O7{~~V>Օ:j1a?H>`ȸ jb"|ONpqJnpʼn0 `׋VhU{B4Hw172etvL˅kwV>*ھt {Y$W׊T=1^qQbGW >m!4KFj?T1\Ulg> Vdz FG_c2 4IsGyDM14ƭǖb2,z2y%k&`@ 8U}8d?rsY$4f ů =J(@dT? r` W8Y>fiOHTcVŒ%vحz#W9E$RN+FpC/|KjV7?y%,mUc?IUTde0^8jz(SSB*G8%ˢFhc.2RIi qD 1'&fa"WJRbk_MĈcI3UZp{'Ta:@)0eLM}f(fbsbw?7< 3Ku#Aֲ…J8ҽrDx@Y8Bs{?4g⧌?M=Z|%"`hATҟGK+K[~|ҟ3ȭH*1=pXI^ P4srQ?5)|vH$MŻ'_Q4C ݣb}(V,XRmꢺlk~RNؤ~( K@#=X3?? Ks Ə'B{ !Z3J\L% rzhJjlM@8;4h鰴ZFQCpaAs2r N%RDGYJzc}SаX܄}U ]W:5Mn8R 9)x`wQ2i2,1cWQz݅n O#@f C [#8,G<,w-sIj726u/#ڹk `"%6 :/PA$ɈR>m$԰ȕ%?1Fyob;/M^')9I0z@-S4z@;ŧ6NvمwTRA|%,b<{O=[G}ے6ZHqO7oZյ|;m|+C]? I;@z|Tʕeqe GGB>JÔD/Nk8#tp,́HGLLR檱kˏ6W7VWoPJnD}W>^'LٺP AؖE#ZƩ]0N-۳̱&L@Sc呹<>(U|Ege]eR<j~ֻ×ߜ*++W'䡔;L*؋J者C'eY l)f# hA xFIp2eXAvvuD{)Lj0jA!biR_ؿv&^V[;Urc/+}qf (*(DWb7h†BL)o QVe@Wr"Maư!ZǰA,bs|Tni8S+c $Lp҄r>G}qi;V:!PHD8;9v'O0ov`+FÕy$*0A)Wxg?WRŃc aGb rZ"pA%Cy" O1._9 ~"4q@" @fv^]!BMq@*vh[M.-742^u猜JS>3kd6m5D-[H&h$v2vVV=[n7jol=#&X=9鿨KAUi$Ls@&e`SJ7l4tBJ*S *٣@+O0^klw3jfA2 HG Ԍ#Y-ÎXG`x4"\E2\1J;,R;σ]M (#ˆ[|n=%!(ߵ;l[Y=,Qd,MT6ʑrm+0qL|;PW3l/ݔx*XmfVihM7yØ~?eǕbN+ؼꬒEx"$?HcLlZ+- )!g:J|ŗa M(,)L#c" ,̩H[q{0 \2(ApR@Ě^`5U%WSaZk7ZUh  4-=-0I: CN KR> Jֹ ʿ5G `="HԑH);~f#tIf^7lElj65bgHœHV5U$mR.0(K7y 9 ,2o+)َc'{G н1 {QWyu=Vr_w>WpNi*.6NQ}|2yG][R6W{PP-V\Z*5DܖGi_dή[. uVy0&2}crv:pJ!LtlӁ9EҬ5̀cCe˪S%n}T|P."ʁʥʻu[{YT`ϩ}hK۵aԥA6F}5 Q]. Wd zFQ$YBtZMY\|9ǥV:QJJ:f~9PD3F_:ԷR!! cCo7U Ø\a©D>߆ T2.愂UH5**] |8~syyͪ_jdeƆp8D*,V[-`,0 kbNQ`]$gZHتmU=pR4_YC=5dEkL£}|2bxXJrʴi9KvqnB4eO8' W'j%9D&>?4 ?T+0 d’QT`tM!cycH'*RliŖ@ł:^ Osn d myin2Gt"v x_q[uZ-9kB]Rr>#VxTuEQ+7 5$оCYf:`@^8n* [a4*ԠT$nO3L܇ MD4aKsٸI2<*T0 kkUr!zs6>} I@|g䮳H0xX #|䷚֍GuRXQKq+"0#%aT}چ p7j^Z]\R A>Z~d]SשQEs'uV ZeN8V/+|?}7>k+2&9lBzuxǤ٣4: 6TA>Pa }(=IkѣGL x) ]Niݩa#b,f5%J AuZ{e2^;adP (Aۍ8 8ѐb/0;Q;3]xeME ^Aֽ<;OVg1.Z RUP& =^ءNIݎR"%dֳU!շO?B~S|aŘ_j{aTN^Ȧh"@4WKQ5B%2?E ˉ$#E(i[cA? .=<i㠥^7hG0 <蚾 yxʷzh{;"$Qvq>}#e5]i>4h9o_b3 'y̓ =ϫ, d|KZy `OhIXu*iJX|fA0l9E"B&C@&(u;Ô}b~5!0hPo|˺x`ٍfO8:״DO#:cubނnlVzmi"‚}mǮR'Q箄#gw%P6u m`@l0HQq#f@؏=O>c0q+Wǔf!zsN{+JBNLW U\o9m &B᎖xϳ0*ϮG+ 3=u.|LQ0"XY45r*𗧂?2O4̓A<>FիygT{GGg<J@(G`[Z?T&Q`Ym qdL|C] kˏM9WYGN?J/FUbLP_D**~km c x1xo|Y[WeN:+ܚO_W;lELjDl~i@7CaˉAK< \0zW)0pYE|d.|V೪e!E B+ҵ]N5M?=!~YJ ^1gYh,ƅk7p=YLX7XyT5t#y(9 eLlnWYXK<(5\3!vc&HILN((Rԭ0DA==H-UJ# yCc q/ƊKĕBZ" Eouٚ鐙 AsR4zNfFͿ/*!AaXgQ2/\{=$2*EX&OJ[] ͞ .Qнԡ&}ɿBUz|=}nzA2BԺ5k-Țb+c+N$SQmhMDg6׫6œ4ƚԻW_$:ĥBL^{U/V֖`Gȅ2.zK0XcBk줍H1/9Gl(7VCu[X@I><)# z'4jvE 38=\6]/d,?}Ϊ$EJQ/JN渙r)e /$J5jsCkW +ML(&ĿpٕW-RHceI5M(qR`t"PNprU*kXứ3EH^-%{eRҒdnܞD3Gu[4nU9A bZʄK:-Mӵ=DJ|g{^ūɓTski'w^`>};#{Q;gi1#^cF|&=t16 zTQL:[^@ꗳW׆XψJ˸]XF=26Z9!mb^ljV!ITLFvoQu/Gihs5"Mſ!Mk8:E)>ehY u-ŘùlUZF Dm DǼLH6 K6%`&P Ӊ88[g찮xG0YUy^lf# /`$$$\4)rl@?LJ/N~>G+O~:|sCW'_ۯyn룽c`9xG9{ux?FO}P;^??ym87ɟw# ǔemQ^˨x(qgb T7Tx}#ǍIN;Ov?^||u{]^|l../o/W'p]>=>)ܝTUO_e\qxR(rޤ_s i `ûiT}罚SF^f|rLvcݣdǔJ i6[wm|Sο[fcSD 8*bwtZkU~xD,$27wvr5 *"HyQM?j4&9bɉݭkM^])E#\ia#ݽ{A)2?tL*KTz)ԹK 3bTLJ䔶l*@E % Tdw`J;*qCҽ'qܣo2!q{,Gf*- z۩!EaUx',; kzb,'@SpZim=)=x&{E+cz{Zz'2쒎^BwV͞zQ-W>nUV<>`DM&X$+J8^\U3 g/Ah=p]'}M+)Qvή_8L>Fm@ӕN<__Zz ?}/Apv~N0Lfa{ ,O8;*9jDy$$tu~$tDB7DBKQ İanov};/"1vGMP# qonq/(EQ0qJ{8lo?}͸ی࿋?6ςl3H'f15FAڿyEsqʣJkZkgE{!ga{ hU%,*ᑎS]$0f4y ڛة++yV>?_B /snii4V"X~#? aՐ$՚?/ULv8Lx1r8p<ԯ hQԤ'lMIŧS:䠂zLw-A_$H/8FBƒSLqrLF=wЋ jEƠl&Krk 3#S'e 9>bIF;E yЮArK9iQ{lhU ܯIQW3t2ݍa&;#թ3B$$>[;@WWIR~| ^[ xW8gN³*{Q?E) gȐ=ȟ89_7vh>*swS>3Qdo5 rsD۵g+p:G!oJ C&Kވ AkGc85*= - *RTy*V%x |4C=*[ZɧrH?%R*C>{xK3{ b^q"ºP1a$, Ux)#PDEEEi-[(mUo[+dIt)@B:aF]@ R† _ jKK19<·cNpbvo}50MZ^g0?kߵ  *cW'a'm_hcэʳNs0DG^]$8\HyDJ*. ؁u7> iH$iLwG+#3t {qFpĦ1ǍFB980w*2Ju^Q^\DI-icMyL#G، y=؊&[ooGE<f%鿤 Wui MBGy}~~3M4qs]MgA<$/*PtlL;a䦐ۍ/x|vu%|Q`@Vueq5;JJO47<BW/a;swF(d;v8Uj0A]K4[ <( 9 nTH gj_7cpC}dul,=́M`-߶Ƴ憲ÍO!ʤ٨@Mf{GےwnI HX? Z.ۭ|D]j?|fc JEztཏNr`Ř Q"lUagi-[tnt: 󖤥=M2 Gj~\͔4# Smd廩\B)rX6H*J~0? w}ILd{a0lK&њި?6.WmBK+Ciܽb;`NoM9d^-WEU+͏ᐫpu-K2d:iRGV?#\3"<\{DOcR0rjP#lXV~5oT޿|U}q?=Ogw?kPh 3%)҃* Vӛ& 8}_E0mF GX1T?Jm X@#`ddOPqq+>íްAA(e٠č4C d1OJV7z& (٢i2IB1B6"3aav5=MЮ]A N7ݦ6dbg7O?i_GA&f< [)Sm2Ҋ8}Lr[Wq5LU`)`!a::!mSS2HaNRo dp؋L~Μ/}kzh{޽V$`>0/x0ɝK~0;̦0ᣧjn9[jҀ/^;˰]IwVsXRU+u|;{Z#pGɹpoԫY5 企z ]KȔNBP6>G٢9raCrи)뒥 @~+SuxQTxtzדɓNA;n  bCw 8do(aZ]y0r8:e~la HO 3cGvJLCfI8MGvG A<6aUPJ`Y ݈449XQZZjA:[۞!QWʙqE?!m|[K2BD2^Q*N0U4[Jeȳhp%²c C%qic93({de$l_Q`AGv{RP<>9yuw؃R5?s}}ߣ;[Ut}߈ӂG'n 1: J z2M|U@M}7J2mw:1(C)wJU `W\[ABsDT4PƯ␮n񰶁lkDiӣqHRTJ6kj˵F`ֻ}suSZn3WDyMu?)T+ƬZ~ CYɿ[%SzSWKiqaow-'P9M߬դhkZі.ڴM5vax?aBN$ԃLrGg6*r;Gq"fkK2ܽ ԉoI%0&NYp K/+2YDxFǀ*oPxo4r;Ldƿk!8r4@ih)T0YIGuutmQ pVPY߸*; -x_tХ5iXƶls+ {[cRPp{OKί0l Ix,NL:ĢVz~4L,-ݼxX(d3[}("FIk2#+a9v{ɳge)T?5λݛ6*4(0>aD쏫q|rqu}G"-mmR)YV8ʐ &rO9M /,G xe%1kJpH-t1~;2cY27*w]֘IIoV'o rbJ:iG,meRgtt\I8p3 LPVc<#94hs8O񛹐CfMSckQ<ֿ֥e) G߱%:l}$D4$'"}+rڸw)씟scl?][|/rI.fw` d9x-T6.W$20h'Uު\ӕWVkw_Yz5σw_eyaAҾBh8걅,Cycep5Z^Z~/n.o^8%"^񟊫3x<_T(w0{#wR(qRHH$?=%7)Lcg) 1gx" @a 4uʂ =G BIx?@UА1aTڦg>8&)7CC*8(w|h"+(~h׃AɀիG8D_%xm)`a,4'JVTŞRb|6&6Q%ƒŕKg's z=$gmEduy-eM"tpC)C,iI2P$uK.G㱅qmv6k7Bȱ1&b@[&QFoWZ7UmK01 YHmN[YlrDy9cj?܃[EJ3*Z?ma(!1Z#nS]5(lnzƸF)>e/`Sӂ N)zP "5F΁)t:-eC~LՈ*ge-Z?fyL;n3``(J9I%R;Tx" "a9I-K-I}$'#*!SHDx~fxo{LLAaA7Yn&^%-I%hj<A+aѝoӨ&Bj#OSqf oDMeo~鎟ŅjS&M,.0͵pW`5}-m[k6?ct®G?y젡hh?mj*[SA{W$=d YTcCZ#ҶBD$G(haFJ &D.4Ë́"drKvpH#ur{DWAcQG'JF+4A3}n PA~D!%zc?AK}a5%}ƨSTZ i/ nQT֗" m W6D9g$tQfԈ}k.?S+;y1o`߼ϼ9}tbPEC <Vg3RHi10;Zka505Oe7aϑD<y[~8M; :B^|Ʊ| 2Aej=0xַ脓 \3gQ]`-_FA;&4೬2'՚93o}tv{U,WXHs9;\)q% @݅pm$Vnr _t5cpo5>af@*o[yY4?#Vk֒]vqtWf,J:"m GJ*A*E倂W{bȌAJlCmфp{lC يw24 }BURry[mU(r>7*!=|شjx30. f36 MnQf{}{֜"R'%1^8lMgA\\9~ ?IEK4v i"Uf- fC?Nl!^j LwUN}S;|e  Xb;zy.rC^0+u3I'5#6 #y4A+݊*EW<):L`1|G[@caamG?SC\hbX͞zSI0!3KNE/Dq0<]{fJʁM Mq8[]_/(}/lN Q e%4D8`ch'e+(mIhg7UؓQZ4h#ИU`(`=^  }]75e8u5NÇgW``<]LF͏ &\)BE2YmiE.ۜt]w^=SmWh3Yw$Ll.ּI0H)h/wR K­{+w}[[ͷ X͹ l1zd  u2&DP,˓[8;#[L_ps{8 7j͋Sk)˹4C(6הYJu?,q5&kj!zse6>P 1۸~/DR_gzcJ I*cK.!QJ('h4>}$)žӰJci/ttjTB3Lw6CmIrVysSig1ܐ8sW׸ |,l4XrBK\(TpL,x%%ͬX#P!E=_:3aPCƅ_(wƫ/ii0@1@moR[OpʶŰc 7`'6IEV T$qϲӃZhru2VG[B$2̜W0nn>BaftӴk \h1i@B|=h`$eo0@ R4rO@Gg!vhl3toŹ lx˨zaQI1ʟ*mSOCn旙Un^FXc~,FefRM^ܝgQ)E̫)j[ϼYcN(',9(̣1OZOfscyK!gܶP最))IsO M&BYՄ"2?ҥJPƗo1:IVx:r.%dTą*ar1ϛ b?w"$:r!4sol2#/$Gr*L9aK>!$#xsS2u*V0n (kǍju:9H+K6f&li mj'BY lÔ6j1~CAeYoM5o^`2tOOv؞l9EL\c8 'a#ʣzys&Z}h7%*ZVAMkY[9ꜷ夳4 Qjj޷ϟ ے׋Q>TPdFNt#Q$(|KsR .6]6fts þďC9O''^/O^6~ c XBQ|rj ȱ#Ozi_\)DQ ӧ D]6ɂcu/ijGB{' >AI~(KEϡ8Y@$`oT2~(w]ÙPh6$tՀo)HGmIr uwdU"ޒ`%2AG*xoEӣ~zJQϝ+]OCK FS?TaS {x=EfCOY`.* P1j&ǴgE1H%EBm]tn9Mu0Er4>lwxyŖ eaYjXX)3L̎lGӢxkT)`ЄvyVMt`M%ܑ`ϓ);ٰǔOՂyx{x sLj.n) |=.dߥ H>v6&5 B5l6V9vss0ݰC+CѬA@vs1(9斲VA&~Ot8/նk]`oTȆ%~͖]kҥ:_ln{j3cF&?X˽7'el`|U4>ڡKԱ巩jxpg_4`ZHuO, ֏Z\T|9ك6#zF:s|?\*dvFWn`Yᵀٌ:.|<͔?>_B83E6U S Hqnn6@%N_suV}rRAH8ŁQYk~wjW)Ft=xbFnDZ*}a`WUF*-56ΣZ+k7=qAdȅA|LY#5aݷ~"0 G mH&{lpU+XRg.p7p/hػ*7_+FYYi~)0VD_ xȁvX!Ӑ-#Bs#cɖN]R cڀqvih~a?z- upj-qJUF*z_b|;Z2A!Zœ>j~ j][^1d|Z[BgKGХw6\aJѪXvc*G?JM4U2,(,*%ˣ^cV] '43VGF0~U>(a]*œ<978uΪyJ;uB.E[4^RL5bfr^JK7LԄ🬬]x!͊-2,s݉s+$ 9aH3LUP<U2:lULΩ#gJoSoKhM ̒D_U+kq L0:AUxa.{%Ä 3\Q V|;aUKa`8(zd_|Ws) E CpqRUlj LrW.6X.i4TmS%P!xufc8UN'lÖDܢfytEU#R^(;pkcg'br0z}=:FX';n[?f Qf+ d̃b=50U麈Et f6{'rI<"Lh8 kHk_L+bf 44x9r[հ_t/*J+l~ G!hKִfř-^/dnD)\s%܅MBʿ*O M&-CO%HA* W7 >th%OCeoJ L9ބ1 k_v(B1֖9p=,_kI*ŗ\u2D6Pe̺k)8gUr=-^l\VG)e%ßxE$8jWW[uvoWdzTL{Tّ)F.!Ԡbזw9,Kf]{cvRP%@н!*Kw/*sa#F(ݕUO%]I_@ u1^Y43IeP;xڨbv 5&%(S5IhgKdq3n=*ug踪TBM~SQtQgqg 4LlNթه;zDkP)]}GÂP0R 5Hy[(UZGFH50sufS Z8|x=aՠL5tAŘjp<ՠ]}0thR]~~~pneU ~4X3&-ZTʈў ,a7 .RVtx TG$$z&MQ=3XmOTjjEUZq~C|(C؇oPCZ 5^o~<~t~n ;Yusj =S.ËSi R᪭_gOʽ4ن,[hG[(&Y_ۈs R{釅 G>%Lbb4v*-rH$EN Uݎ݌&L,Z6NdWkKno'SABٗ4CP^اpV)ݩFj %oӰ㫞JceKn67 [*3%-3ӭ4gPPeWh5c[&Ehx(TW #8TyPk.*L?#ӛS E!a{ MR6RKG*,~a+#EJdYEL񿣍B+T%_7Ύ*ir-採#5' &~͗;x R*#~{%ٽЅCy4yr5Ǯ*mMUÈ1^ T -3H&Zb_E/2}Wu{ gUEۨ dy[\ j0*=Jrܲ*Q cև+l+n\~;X9hAo>x`QEKn`a~^*l3֌hKoi7~pX\\T?U}hC44pY/zFtYRʊ]+>/C0Z66| 4p LHٵ E}p&uEU zER.=.*pRn026쓝HM `f\%[5f6b)0@LIWRXY|$0>Cw3ӎ̥l4I~I\;ȗ>MS&N)bb!ݦ9vqSa?U'k\.Cyp_-g\MM`W䤼<5SùmbFu`>Kd+8uAeR8* U<.FpTخĒNGlbVɮ*. (jmήԛImhr rlg' Ȑt|^nٮFUYUjCn6:H #٫5bCfL"2xtV>k[W [8/stE[>:q=YL37-Z`RnhKg[ {&6ņ.L204(3E hae d'?uj.zbnr24z,bWǚL` }P2f^G/-lޒK")uH:U8u ޚaZo 2QO*v,J$1J@W?A1MrѨX9Ea~1"yNJ@蟁s<ҨL=ժ酘~zs/DFIo۬(& cA;Wj4Y9P;4$-1ʸȸ\IA 2ğb%&rwt}va1\fccgO)ҦŁjk[kcL)cUեcM퍨Z^ua:ֳ-,n r5o'4 8s\ic1YZ%Vzt }? RqzstC眅~)y`iACvITij"zB5HX"K~7 #m{V|SgQP +d\MӚy9 gN4San. DdQ.[Gw'IbV~P{q<j>]º#@ J) 6ucHtdžRm=. EeYŝ3 2ʴ5 h0F^w_xvbR8# ̪іB^iGwĶ}HenB岋dWfn EIHJED4 \_)X 5(jS[E&c0 QE :e*d4Q结 {@Aըy3"]"`@Z0 mz˓­s2֑ `ac_\luJ,Y5'*biɆF-𩺩[p}cnO KU<WEXܥo&i գ7AY{ LW<RM`JbwϠ^]#,-t^i.nP*Hо@O}CH/.NQKFX ,4̻ôBC4AjUdDdzz}T;.! n .eJ!2۷Gsw u4O4#K]4JT#e~L{ݱs翐bBr̳Wa&7 .QT $U(=4JfNyq'5Su@ɪ,:yU9U 6P,Ī$ /RS3T .'T ,s4$ V_ *. O5@Y2+(wƾX,䊻ZGly>&Ns[_;Քo&5 1Pnyn@ThLit>J.\=`p{.e/nЏ(__d0mkؕeP&睜#%eʎ61FƋBx^nhmS}`ëj@un6!l3cͫ{;"9ᣑb"XTGܜD&71Yk/cJ7\K̾ ,M#J~:3>8q'WsTp}j{wt\,-QNaQRXDc1X\P| hng-xlL'+hiE8V^49ec:r1=ԀvA1ţmB&w1gǨ >WF2B&*a@4(#3?x[6eOem0J B UH|g_ }4[/t %)FceC?0`2t5l|mZVAeQ ttYx}6u{E1s6iTA'z7m[_?[k;0ЧOG6=UBbKK'_P@6#qOh{Uph "Ԛ&ox M)m×Sk;i 2w/>Ql֩ElEnθZI8G%VpV (mr,봡㘀dHP1F.쐯GcY6׿Yo\WL"bKVZ J7 6oOSՌ7IĿDuEMo^P\GߚŖ13?w([:a+GϒsOZOfscy+:~܀ڴ~G(6i7iGk`Wu+[g-vI]nΟR<䶶5`٧,/.mW?۾ q-hcm(!+TEvvuĢ+𶦤0 VQct)Z}ks?_m?WWO憃?9g`_y`/IMӐ"@ E3%Q/ #E $4"s]= 959WN#=6`Hw[ۜzKtp,c)9OGw|&y%(:ylYiM$=gðW&HJ 1K˫K+̮m~o$$cs$*ïץ8h:>~8g l[˫QWK,#WėixJ?Ӛ@XF`0&o=Qh @gC?!h6?wJVX'/L"Qa7ٽlƩόD3Aa;59e:QyL_VӪv/{^ G;HܑJNxt; VKH|hhqߠA5mQ SbJe &o"=9SbN!fgf|[*>?9)L!')T+V=P Sx}XD#D|:IJNUbɜu@'kgYrqYJdRQKWp$ $&*Q$qbY&Jkϙ QqCDVy< |ʔ&H>EY.ɸ&e&s렪@ɦ֧ظ6`.%A2i-ѥ<|w xc6z=:n<2¢)U}O)NҭQ 逓'F4b4 !TjuA[ '&WU@^p< uO%Qr5J.P8Ua3 ǵJF)a!A(SGi>a NB$ #c@)C]~!Z 䱏I1 sLC@&p^c`~MħB)|K;y*03>XuDdRo+08LXbj] G1+_Hʀ/=nZr.Yʆ§rx(R>271-8_XOe.&(RGs;/jКQ{-v7KK}RWKJSqٶD)dXɫj.nI+E!fscދΒ " R6]4>=(6@N"8Fc]؆M'lzhl12Nݸ׋/*aۯ}]a+ N4- h$ >rX0C?gpF"-%D-C㔭um2eLZ6u" )%$5ͮ= 'e6p?ntQs8$f%v! [67ñ2QGvyn|3'`NͶ9}e be̛$>MFf`d*[Tc tgDX*GI淲t |X(~7DKh}]'裯۩[^0rEUwQi6|3}7MZ@fqP"f=jJYLfq €Il0*ӧ2NL \}HM{!fP-Hh-A5Bw\&܊QbՍGNFc_~5~qT1:l9pLUv'Y* BhjA\FqS?Uw_>-Y"y̾~(,EmX_ih I{q'r ȳG3C0H2l~024Pe!"řx2曚i-ShE9[?Nׄ*j_8>}C @J=+z*X^2t.gV"22)SY%8#f ѷ86-x7d8$mkmw Ϗ?~(-py}`{U݇odoџ'70X&] JzWWF?>xgmH/|$F/?]@]MwCl}3">$"PTI҂w gbk3ƬBA˜80e:kdIۘk&m;A?Ky2g50BtE*5:#$?C?g5|+Y{ k^6"585G E| :h=vD7JW,pMr knCDxQ &!oaݬfl3ˌuGMхg6g.LagҠZC̀Vu5l! ^xBW@/slo! kk?uLE amMX*-_ &KQ %'$^a2FTM cIk@IcÖ2n4 DQLr\3Okvͬ2h|A8.Y(x6JJJ ?b8#5o>̧ZRaZ(0C_0 UT`qqM?Çvk2HSu1 ẙ{0nM`/@ć7Eb'*|`jF55R5|ngޖW 'Օ.) ~=zWh, zuZ9y atDVԖ; %Ϣ`za7Y#]qLg/ E40ʵω*4@XC\z_I=]}o61荬?ooʣǖkX~Vp* U򧕮nٳ-}ӭZ/\eTj(J[eVu*'$67 lb[]6 #8s!6P$7M-o|SMβ&}P?kIXb].zP{ 6jOg/W\k\W9hR^*ozPxPr6sPϤ|'&nj_( ~?TЇ. 2ըbVsykPP`}<+ejo;u=__[*Wi] 4[5ۿۭqZ˪-jT̹g(l3YȲ%g5587y lrQֹ΅ MJ?'swC%]!E.BYBl-J}bŦYIY0 yuj 6żyG0 +yJѼ}%gWJ@VQрLpƸv({ڐlaF#p_ƣ^} JgJdMԓBY3ݗlhm`=sV( G uh9!(R8MGƨ?}cS Y$!˓%qwђ$N$NDsacUCxMwh~ OwmnDT7z),bMfa3l]V",5b6LX)&Zm^+zM)cQFq6ر {|pկ8@n<h6mQۺCn0>_Ot4_5"]eW&;PB>[+:+ԦRf>Jk.ģ }a J̰4tfk}|'Lu4!6^ĬɭKjnkdUz7Iȹ&uY: wYJbB[$Vl>)<h)/4T!7VpE 1RƼĠ! #9$\%>~=\ n/K,i J'C5j>8xf*i UoP+8rw~oOY)rPjZq}yL5IZoI-2ӫW y+ƌ"\D9>{[7բ²(6GMzԴQ+_y%WѪhz^vG1=z?Kn󰝩Ys~?+L[vz0%^ OR+_ $F}[m66E}_]r_}?SIf.7NPA8t:3Fu@;apQ.v8hvdTդpn Pw2ߔq {5LV_ ( ,cHaR%kt+~b s)`;GB\#KMEx}hy#_T)Hn0֢5R.1w+?"V{VAGv-iD[mmFLVc@*DL+_آL0:GXnP8~e﵉Ǵպ`s#[JՀf[a&2o_J_L!$] d~m`ܚkVC 7L0i%wG>ĨT/" 3N5ut)-P"4~VyK٭d iF"LJw:3Sq◲׳mW0jQ8+{_~[Կm?Ҧ¥:HMVb&?!&J'h|_X2jy> ̈́~;>T J׺ɘC χ)0 fʚrPk%3Spčs+n:8׌Q0ﭒ8 +7G/C)G]}ooiH?Att ݀Ϗw KH!Aԉ:U!ecxph?cCpq&\g1e^q)4svy'B;Idg^` aoS? A1]6׎ { )7K&xve h4@~_ddz,mar&u[1mIdDA^[H/ef:+( l ƀjB%![lFȸUT:NIC_fDrcIƒ?/tI `|]G%_$PYlIYƢ=FD$;e* " Ĵ]rAs7:|wyRd2t8{$.^䁄I[[?!AoKSu-)paͦrc⩻l^^x祳Hë/CS2&-6tQaw~?ܰZ1'١*ʩBveĺYƔi~[ m@펪תMamc: Je.DWi?2AI~+=gddԲ9I*qTz[ᅫrR#e[V=|z'3}n5 Ge?~;*ޢ$AcSq]4c\3=:;=y97b2C!vbo5}$\/h@ʨ}{9 zI$\ٻhPIF4cЋ+B9||Դȷڎ@n92O(9 M_'Y]o{Ëy1_]_n?7W[̍n[\%;Qp6( ٜ ty /xDS'SNuvvU6 I2<~9U4jG+uj04!bҋ5S&.0!$͒;`0نeDQ| zvwQ$*;XmRד;+*<8=> ~?={s{"cD) ٿW9 I RW_9ׯOOOv^}dPa8c8_yo_ruAģl:9:|sXz;K Kz{~t}| OpÉ^O8Q7'kkJlQ0j/K?.!H,jbGڸ+dE|tfd^-ga% v0SwXtSBӟyeĥ%p\[P_U\s(5ov_ne6z$zU$KˏՍ卵'7(.iA1KoKL'q@Hi[+2_Պj6PWF4HXFD{Ĭ*2oѐo VngWv~}Ӝ㮌Ut㮼oy[Xv.J!M\# ^ 8bRƎrTA" %_faqCj͹OVLH%7A4 +#RPXBdUeeb~'嶗XO^a}U0dK8CUGh,=h2{˱E0hjWڰW>U )R]4s+'E__n,o Sr>bAWEU:DYK2lk%r`P8ƥ6eRAA0- 6nt JrWU,ońnzKe>,Inm+&W&'6B M.>(PEm7VwE͍[-,tUFUq5>[US+ 8:I-˭ |3S&9RoWQq[W:T _*nd*8 ^0lRNW .HVŗQǮX:16-ԟ%^+ƕ[1@U3 y 亃Wo|TQX!kv%8;r8k*SNa00%+T"z P"ȒI8\o:@|JAG~."IHLMcuglYQ(uX~t*EN'Uw!~#Rφhu CB[M7pEpj͟%xl.V-[~|iZ1!`Eig$Q bDo2NWqje|'@6,ldC bWY=D/A5Ox:N$-f0潤ЅRᮃn!q)˽7'0edN>ç?\Q/wAxqUZ71rkCELu* 1Y~ tR >FI<@=| zW ˽J*CWa0H9s&iÀV5_WDK"Ckα (zX?Q 4`5T!7Γ0̪~,]޷$çP`3؆$erln6O2Ɩ ag֣R66~TWWWWWףP/-:V+FR++s]zRSVb)V[ H<%#1UN&P*/lQ2e5~H:ìœ8x8gƬ #E8 'bh|6Ӎ1ƣ 7s`g.:i1.z5z?> *ȤN̋8NFzyjwkKH1OlY643|~ DXIqSI6E#xbfrécLe/7,ekPwaOAUh (G4c3F1 Q:uBg[WU>)pqY8*aA9TDu#- MhAWg 쳘Xs' o ƄGIwz{_"QB("v 6@0I'S紅3KŒFsKfq*AcNtp%~T"bЖ^a1wXbWUHaB&PQ'5ncf aUn6'B'yg>^:/{ L;Dq.N&}(\ZDBC^F H0e3E"Ǚw]E A`XSh1&1yѓ2r&54=G/r(R1T4&Q&qpg$^; 0( 9 >&=h޿_{qkcCwO> ,< U]f Q7}O7NA2a[}\x!rheH!E mx4jUQy,C}M ;$Id^ " MC:LUWm``W0@rЖNJcYIfgUC Pt37?P![13^: :a/IhC8~6&'W#'ǜ5iAdwPwmDu|e"(5LmvBxtUm"ZBײ"0])B7M@mؔMwIxld;B774`0 axu)b§R9xPtA6rS|;@&|K+6#fGYu/0*"x̂xN,E7,ƗU;Y"=e"mjtR\%͘vx1>GQfx!ǫ<>VqVv?>>L9Gs>0~y*:êe܉ϭyjt>vyQ}l|< 98a mbIJg]$]Q+u1AAʫnL ^l(kbYKUNYd7NqP<UZw/^UX֥a70vrn_~} ݿTP!Z7~\QE^5Goe37WI&2QIᨯ&KqLQ,p~n,DHl/?A a#V{YQ7\D_y>&/XCwYXmMFɿEr1JCr N Kf$?l#< B.~& S2pVb"6V캖s`[.ZQ<_֤6vhޔӏ$ ƾekLJ\L TgNv!eѲ4GNb{JhZh1 `49&mLF)qR_h4X|MJ~`mS.ӳK_?6sϭgN?+5MxeˑûF(KeXFD'*͘0P !+ԋK I& X'2 6mMW3[ҍ'mrQu>Z'אxq$rWiSelBΌ4tJNqRWj(eeF9>>Trg8QTF`12T>T)cJT $\WH~U(b"SE8=R8ºU$W65abĸGku2iL/ .?R%21#&qZQ k,G`~",J ;-5]J0oQ1}>vM\3g-^.s;Q&:VDƼczF9':a4A$I'MpfZB#@D1f*+&ft *;Cz4G4{#إs zI<k+ӏ.TkiRK^ mIh &L|r@a@~㩟ePlv g"Cdl$<<^*r1xLǺ7l3ˎKv84>OTs ;o{T4-RmJ\QGuN1H%\*da:clY ̏5D@);aû>j(e? Rx: lDs|UHkk,;gN\"a Ыf)9-*_`y2uS ZQrkƥ(fG@3s#Ӽ"y3O?"[~lU/#?0RT *ioԯ{H:*3(LL "n[v_qxHa5Yǽ*$Y1,ᲽqLAG&sHAO.$9.fr^qvq$%c7ޕyZinzgO>Hm`7m%F L:$kl&r䆈LRq;*i{տy~M,rxs:/8#B}sΙWMʖu@:!VQռӦ!y&4r"2$2L$ 7Y(/j䔗0:3,F V솚s̴8fz^ .jy6x?xru)B9!ףB)_~7Udv~0dPC9h&48><:~s"PzܡH^z 8Ԛw5[P]3c(]D{u'C;0%D:r NcTI!Q(W+Ik)4", XDIQ[`OЎ^ef8g&ԍ[E|dǣok~܎OKSt ;} !cnBLb6NySq9#&9Ĥxwo1w y&6e gF^wKpTq|vtb:m1 \o!u(Y0 Ql# -s-xC j?*LS XW9P'!Ο&UO^Uksq<b}i沏D 8 $7Ц1oI'GGmnPfY(eq}4$&VG&%q\ZiWtA(fO';>*aGúX$3Q{$h WF%DMp rVT!MY~+,3JIzi̲[ZM1<tgOV#qjG1-Hj`  <.'M_j~ Dvpwewf`۽#fK捛 +PIE5ϱ-] qx!J'7RWf˟23bWn5[vSt9Xc.IeuN8= f\%\-NYL_&]b* +ϫL\鷖`?ǹg?57 l-3C~0л(A;|sv[8(ƴ(֛7mߺpRL XOgQml8o4lh# AsʌlF0QfI0_zՋ/ QUJ+UN2;%+Q j:q7{QPF4/ <N0" zU"AK hǴcԣp"ꄉK"zhD֙jC?LR׍ naϢy͝Ș1QHZMNKF& HRo*u,82SVȸdO Y4T-3@V<[D'6e-/nХUѢX54*4u3p }+aM*UJv͜|cBnF)܊z\;"+ f\mQ[зBJ%{!V^\NA([#Ga3܈39$5'{B7\/Rl'KRY#ߚ@|tG%Tmxb" 9ꢜ}.-H 6N4\3 w{Z @kB悻lAsیSHTgAejS"ϥ|\Qw<,3뻝w/:nCDT+wm_>{qR6ib,Q=QăJ"8}~YaN%J'KZ)@dzWI#,HM+|Ӆeh5`bTZ  FN{). do-)1\ % Ě*.¡UpL0S-96Bzr'ǚ=|MV"(/+.38C:[c.h彲ø\,E0k3 Y&~Zz7&b~h6qӔYQaG=B؃-S:@`!fH IWJlZax\~G,`[؂7;qEhԉ4?c1Pp1ƭ">!'t/5?ߐdd 15ʳ۟( l|:qP͝@lyo 0$< #.W mLO?n\aoMR(0iG:)2uX.u2i 3 *] HǫÉК+xa('{b0/1Iߠa=6'Ӏd,o*'/1cae¸Daf8Ctܒ{A ?Pj0`0C>DuCף k㆐vq>%8xQ7l_Ƅe9;5-ѶvI# \`6q:k!r</Ȁ tC}\ΐDpΐrmϠIEFTƍh c~|Rpx,Gաx>H ?'O x>>&[2vQCwpab˪>wa9x ?Ҹ?wX 4ɖx^pc3'YM~kmَx^pbpm"W4p I$VkYM$VFyNQ_[b r31,?tFتa;&GQ6H;>7ݠ(=VHe\"p8zny'aJ4tvC1rqپ& Q:76!k꾚)4T1c?cM8sZTϪf,Z\ 2w,}N/ N5'xf"?Dk$fg}I#2sJՖvt_}ml:Yá&)!}ΝC!v;8_2{20#pGT7]X8hf!@ -$tw2Zqʸ q8rHB{1i? ) VOR_SA"l?{) 7C/Ky$r\yxnAY=VM(9WrugMF%R|](]Z$8+T ! \k&*Y׉a1g1UZnʮn[t118VAM{ebU@ҝh$ʇ\%SaDy(`6(ڪRcˍu٤px`R+`A7 `]ǝQ'F.~ `-zM+~&}jۃn7ǡ\V |8-@<̔(8BҜ'xcYC}bhb8 ~xi|ya$IFkWa;(S䙷{v΂Alk}O0|1~yQ\>ӓ^51 ^O4{]븬z>4>H_(B?[%t{tZ,f.HEnGlĺx}(H^Q9 5*f<}YVéBŞ`ArO@8-~7MsuO!!E,tY_=3X2bʰI*:CObbڿ U.e# .D<Uк^qW)Pkm IJ w6ȋ,&%W@\|+&0=¾ܢ8 AP0@߂(<5fENe956(Yt${*TmZ\ګ!zyR9蛩vl~X-Z[j_$(6AB:#ƮZV)OL  /8& ,=9ew8-7Ҏ 48G/mL\dĚM<jG%ED8+8C_[iDHW.bcFn`Ơhɢ2EM=Zok6' 88BORVG$Sa=W?IY#P -6B.P`^<92`!cUAL5CŠ}V浓bo5gu KYjrN:0?DiLis."[Wy\bu}f꿘չ遦Ywt8;wu5LF@ИwlV L\fG`wuhhe%ZOT &m9xL1mg?̭GLFeD'!M% o歵R*$*ץGԼ6 ᶕY4,.:XU e݂PqV%^uAX  =8[ V5 +G$E "ٲ8۾فS&GD|'f=kNugH7S1|UWX*vGh:?vwNlfOnbGj,Tqw'~Cu[{:kz'czlT=#|űSyDSbQ $.*.\3z%ω׊PwKB5 r/ Bݲu x~R(3W͑'q>Aw+;kkw))7dLHu/. =R2 :(u3=}Rv"4LN }{-k6aG֭hd02MIL"`}'>X[tAϣ({,uNiu'Lj![9U:>笺Bq1W)R'Q W¿AZZcJu3Ok}Rk8qH1%9s ~P GWM,kǪ7kWqDܨ}ݴ?N_WgF}Lln,3kaËs'a?WfC>0[Cf%AU<8x):3!lEr3l)ȆVI]gGߞe^N^#rm1&غ\tax xKef4 Nwq\c }R-G8؉V/Bם_ oz{<`ݽwG*(W# ${CE݊)|lB*z 7uEd^C9~u'doB?LӦԡ?I\U7;0x,!qɡ+l7aFu /z N.V7 z`$a=(Z_<87 /žJS=ubwt`QHh@5RU(2jzê{L,=W1JIUJ}r[֩-͌z]2zMzdAW!Zĉ͜9Ļn<£o6OXhƋО~'A%YXW70{0 qOMzÿ9دC,Ý*"Z>c"{ GsF?2#@ ox5f(*~΁:rIgGFj5[S`7\^v~1AȺhp?S@rxÚ+&X{ 0ZapJss}TR(dP0@S2H_O@ow|9}kGV3QuUP$3"ݹnޣvXMd0VvzɃtp YZ0Wgh9pLaю`ʫ;ˍzwj/m4i6FQ.O~?Ks3`7tw _#ѣC; ˔h/r~i^)iԽ]6E,2D+ɟ269G*U50I%EzUY'P{3ηlNh?G53jtZ92N?8,v9 0&1QD9C M->AA2d̅kٲ@2e)hqX4OHM~5DBeE+剻y_g7'3s jh4XSQ߈)-BU@"7aHG5pEN]xسv?Oj݉jZ~\2FLUU86eXm6isqRo_ n3b}CЂ"j3×GeJd$0/Vn&; RrȊW;G/_keek uTy5|y\Fq@|Z E鿟z7{p1rx)JKZUSwJ4z6TgheX!%m92sXH%Ύ[s2n6Fra`!Sy rCg,}h{bRpG{?{qĮU)Iw$mΛXjE nUO1IKLΖKIW8:~nŖ qB Љ}T LNC2qgc~`w Fi`kب&+sBwu`r0M)` K@Ku mJ>$^Vg阩6y [;i,/qgw/3kV)&i6-sH=|ĜY!:dGCܺ?T>3Gί/< %f t]ݢhXyN[FQf]Q%"32IaRИ o?r] A FGL.u4Lea&7`XXٴ\- /DQK6QXV ބq;IO9Aݷۨ˷+/[#,_7d:T#O1h=EZLj: Nr"Nsdǡz!kINkXfqx/s{0hu1nEݻ[-tܺXROnD#AwdStatQ8?޼n>چ_Ǿ/pNѐlI%;&Bm>a]S`ܳT_ף֐Mӿ&Sm&ժ ~G0bݦ;+"`Vb'|;|9Q}gAtW>䵮/ۑ*2gvsZy/;wzƪyVS)O?̦9AFf/>WW:7 jj=^~؋Tɸ.ëG>Ba? J(Vk[fҵh-ut\o}S)uZ΢lԁw6ڦ,7~7n@ծ֯r_e7ƃ8)"_,⧟+J+JJb$e@0E>ד: 9 %8 2|,7G20 Ù?Q:(,;m[ `:ex`[ e^&tf!T K Gb-\Jz cVwihMؼfG6 I@0Tj rV&9QzQ)-Uس'VRZ~/Cډ_L!Ɖ}]4EȈ#0=<8_iUa cǔ WthRUT)$FlRZqED&:phBy{p{J%Q{ۏR_#InOjN!o'^/HݽW]a6Uu<7ÉUB 2Ɉ];„2 `z٘.65lCҳxr?@ո_5ʵ80Y6ѷ]`v<( 6_r'q'D_p(1=drSy^ݺZ7/Wqkkc}Nܤdyҍenӻ=ΉTtGqQ{Z}n|9|٣c'ӡAq'Ѻrww 6Z7ܗz7 -^,v+ Xj Oiuw#ɸ]?YE?LWn&`N@C] _c 2etg`T=]#0Ҩ&q9|;~@~D$-oM>찰0oʛp.=`JW--ANa8" 4tq3Ԇ;-Xu6Z6j \-98i7ZE֭">2B7g|>n  \m3F)`uP#s+r1 [ ~~'viM_9(ٙYvn÷#43yt׃sBź1~F17^q*t>՗^7 ɹMlϠF?l߿O?}z*"TeiOY[byʓjhR=|PC}hhЦC[Ϫhw>UVV4; +*%јzWq˫=.$)Wnp8mìOY~`ow1$PENщD)}\)/E1EbQb$qS0%lZ#*)լ ptJqTpu+7st>Ij69׽SF.pX'KP{5h/2Р ZU(<4]h2D]1&$PQ.mnl7piTϫ2CT~+JwΈ Nt?eùlμ!qCl.cy88n)V̴1PDWf\\'$dqLTQ Ai,q8%^$ػ_& F~Jyyn[h\0Jè 1ҼIf7 \#Q)#FDO ' 6s2JvO/tI1fE1jzIs߶8&ޡL6SR?oQx-[^Buk[2ǏKx݊jѐ@!Nқҏ6G5kԻR2#n l)w.qkQe,KW#-7bM8& j9]aVktWj7G -ZtT9=[7N1 Innmrd[k fa ~BM@|Թ YqZ{騛jo`Ŀ(]lUĪ$j^Ly2lOl%Y?ZRt_*uD>Fh~2͚<#tJЉh cD*CUw 4~3}*mE g]CӵlWoOxB!V' (=mkCڤ_)d%ds&@1 =3'F᧝07YL~WKXqb*pA)[v"q72+r5[ME֎(ٽx< G+rH\\ؠ>R l5qJJi_.PϘLW4ήn>]d̪vn>oכz٬7O[?,iM"3ߓEysյ [3νF^"W)D8hM3u%u'ВB#͠(+%)v4E+0ީH39tL_YF c.(OH?9qE8}9HɫGbyPR )P@"Ë́+2ru.\ǀ$[]Bg#@Z|&Ekf&Tj,vWsW'VDa"1rLՑ}NPc.{Y=Es8 ta$ 1f#kh@`+^",5IaCxrAY" Q?3[t* eOIc&7?'촒LՎ<:枭{[h.C45c#("݊&ϨZS|v R7b!dtZl$!@sX џfC%aAޠ'5#ccj`[OfIa[y^6ߩ/uO}ᤍ!"CSBw)L| e c(ńZ$|$m FL ^Eb,Lmyؔ@I9*h&*Y\hBZf˖zEKO Z:bZĜEK2+ū.xBsA,JxBY*yuz DlmEf_RItYghoGO$#>:[@j6-՗A\M\M`Js_ώ=YΙ9eSc/ln[-`)ttq b5X@1RVAdAC=iN==g0BqH-={M>Qv:0mMy,,EO@:&Gk,Ugi A&'2Qh vI1?CI]jd?8ywm`|#2?KM5~6u}ߴSE P<萦e/Ttj%JPJ)tɍQ8 %LNt9h4jF"YC*{4 孧ӷQzSf q ͌ak^,GGUhU,Fl`+ irv4ʉK,{2ٸ_S+j/;?DX?q͐}'*3Wg߫MRBG+$ OKsInt2q1bp? 1gͪn>aq֭5Vf<𦲧zPwk>5`3CcBIn`pkXh HR@P}cReCIe6.\e:ZJp(f!t.[l㊵+f@?xs/<10dHME$BhJeX@j28%t\E}J?3cR| {q|Ǫ X{<&cwK"ӜP8+Qx)byᇙHA|,m}QLwf<ūhF6\0@j1a1 TزY;JmaP S}%LThP77w :=~%~]E<¸` Ki?>aI\T} .޹kw8hi-Zf5\u4x72^W_Iq\n|&-jnz~n?s~n?7/0pasterisk-testsuite-0.0.0+svn.5781/asttest/lib/lua/proclib.c000066400000000000000000000366241242304700500234140ustar00rootroot00000000000000/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 1999 - 2008, Digium, Inc. * * Matthew Nicholson * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ #include #include #include #include "proclib_lua.h" #include #include #include #include #include #include #include #include #include #include #include /*! * \brief Check if we can execute the given file. * \warn This function only does a very basic check of executability. It is * possible for this function to say a file is executable, but the file not * actually be executable by the user under the current circumstances. */ static int is_executable(lua_State *L) { struct stat st; const char *path = luaL_checkstring(L, 1); if (stat(path, &st)) { lua_pushboolean(L, 0); lua_pushstring(L, strerror(errno)); return 2; } if (!S_ISREG(st.st_mode)) { lua_pushboolean(L, 0); lua_pushliteral(L, "path is not a regular file"); return 2; } if (!((S_IXUSR | S_IXGRP | S_IXOTH) & st.st_mode)) { lua_pushboolean(L, 0); lua_pushliteral(L, "path is not executable"); return 2; } /* the file might be executable */ lua_pushboolean(L, 1); return 1; } /*! * \brief [lua_CFunction proc.exists] Check if the given file exists and is * executable. * \param L the lua state to use */ static int proc_exists(lua_State *L) { char *env_path; const char *path = luaL_checkstring(L, 1); char *p; char *c; if (strlen(path) == 0) { lua_pushboolean(L, 0); lua_pushliteral(L, "invalid path provided (path was an empty string)"); return 2; } /* if path starts with '/' then directly stat the given path */ if (path[0] == '/') { lua_pushcfunction(L, is_executable); lua_pushvalue(L, 1); lua_call(L, 1, 2); return 2; } if (!(env_path = getenv("PATH"))) { /* default env_path to the following as indicated in the * exec(3) man page */ env_path = ":/bin:/usr/bin"; } p = env_path; for (p = env_path; (c = strchr(p, ':')); p = ++c) { if (c - p <= 1) { /* empty path component */ continue; } lua_pushcfunction(L, is_executable); lua_pushlstring(L, p, c - p); lua_pushliteral(L, "/"); lua_pushvalue(L, 1); lua_concat(L, 3); lua_call(L, 1, 2); if (lua_toboolean(L, -2)) { lua_pop(L, 1); return 1; } lua_pop(L, 2); } /* check the last path component */ if (strlen(p) != 0) { lua_pushcfunction(L, is_executable); lua_pushstring(L, p); lua_pushliteral(L, "/"); lua_pushvalue(L, 1); lua_concat(L, 3); lua_call(L, 1, 2); if (lua_toboolean(L, -2)) { lua_pop(L, 1); return 1; } lua_pop(L, 2); } /* no executable paths were found */ lua_pushboolean(L, 0); lua_pushliteral(L, "no executable found for path '"); lua_pushvalue(L, 1); lua_pushliteral(L, "'"); lua_concat(L, 3); return 2; } static FILE *create_iolib_file(lua_State *L, const char *name, int fd, const char *mode) { FILE **file; lua_pushstring(L, name); file = lua_newuserdata(L, sizeof(FILE *)); *file = fdopen(fd, mode); luaL_getmetatable(L, LUA_FILEHANDLE); lua_setmetatable(L, -2); lua_rawset(L, -3); return *file; } /*! * \brief Create and array with the function arguments suitable to pass to * execvp. * \param L the lua state to use */ static const char **build_argv(lua_State *L, const char *name) { int args = lua_gettop(L); const char **argv; size_t argc; int i; argc = args + 1; /* the number of args plus 1 for the terminating NULL */ if (!(argv = malloc(argc * sizeof(char *)))) { lua_pushliteral(L, "error allocating memory while spawning process"); lua_error(L); /* never returns */ } argv[0] = name; argv[args] = NULL; for (i = 2; i <= args; i++) { argv[i - 1] = lua_tostring(L, i); } return argv; } /*! * \brief [lua_CFunction proc.exec_io] Start a process. * \param L the lua state to use * * This function forks and execs the given process with support for reading * from and writing to stdio file descriptors. */ static int exec_proc_with_stdio(lua_State *L) { pid_t pid; pid_t *p; const char *name = luaL_checkstring(L, 1); const char **argv = build_argv(L, name); int stdin_pipe[2], stdout_pipe[2], stderr_pipe[2]; /* make sure the given path exists and is executable */ lua_pushcfunction(L, proc_exists); lua_pushvalue(L, 1); lua_call(L, 1, 2); if (!lua_toboolean(L, -2)) { goto e_return; } if (pipe(stdin_pipe)) { lua_pushliteral(L, "error creating stdin pipe: "); lua_pushstring(L, strerror(errno)); lua_concat(L, 2); goto e_free_argv; } if (pipe(stdout_pipe)) { lua_pushliteral(L, "error creating stdout pipe: "); lua_pushstring(L, strerror(errno)); lua_concat(L, 2); goto e_close_stdin_pipe; } if (pipe(stderr_pipe)) { lua_pushliteral(L, "error creating stderr pipe: "); lua_pushstring(L, strerror(errno)); lua_concat(L, 2); goto e_close_stdout_pipe; } /* start the process */ pid = fork(); if (pid == 0) { /* replace stdin, stdout, and stderr with our pipes */ dup2(stdin_pipe[0], STDIN_FILENO); dup2(stdout_pipe[1], STDOUT_FILENO); dup2(stderr_pipe[1], STDERR_FILENO); /* close the halves of the pipes that we don't need */ close(stdin_pipe[1]); close(stdout_pipe[0]); close(stderr_pipe[0]); execvp(name, (char * const *) argv); fprintf(stderr, "error spawning process: %s\n", strerror(errno)); exit(1); } else if (pid == -1) { lua_pushliteral(L, "error spawning process (fork error): "); lua_pushstring(L, strerror(errno)); lua_concat(L, 2); goto e_close_stderr_pipe; } free(argv); /* close the halves of the pipes that we don't need */ close(stdin_pipe[0]); close(stdout_pipe[1]); close(stderr_pipe[1]); lua_newtable(L); luaL_getmetatable(L, "proclib_proc"); lua_setmetatable(L, -2); /* now create lua FILE* objects from our file descriptors */ create_iolib_file(L, "stdin", stdin_pipe[1], "w"); create_iolib_file(L, "stdout", stdout_pipe[0], "r"); create_iolib_file(L, "stderr", stderr_pipe[0], "r"); /* store the pid */ lua_pushliteral(L, "pid"); p = lua_newuserdata(L, sizeof(pid_t)); *p = pid; luaL_getmetatable(L, "proclib_pid"); lua_setmetatable(L, -2); lua_rawset(L, -3); return 1; e_close_stderr_pipe: close(stderr_pipe[0]); close(stderr_pipe[1]); e_close_stdout_pipe: close(stdout_pipe[0]); close(stdout_pipe[1]); e_close_stdin_pipe: close(stdin_pipe[0]); close(stdin_pipe[1]); e_free_argv: free(argv); e_return: /* error string should already be on the stack */ return lua_error(L); } /*! * \brief [lua_CFunction proc.exec] Start a process. * \param L the lua state to use * * This function forks and execs the given process. */ static int exec_proc(lua_State *L) { pid_t pid; pid_t *p; const char *name = luaL_checkstring(L, 1); const char **argv = build_argv(L, name); int fd; /* make sure the given path exists and is executable */ lua_pushcfunction(L, proc_exists); lua_pushvalue(L, 1); lua_call(L, 1, 2); if (!lua_toboolean(L, -2)) { goto e_return; } /* start the process */ pid = fork(); if (pid == 0) { /* open dev null and use it for stdin, stdout, and stderr */ fd = open("/dev/null", O_RDWR); /* XXX check for error?!! */ dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); execvp(name, (char * const *) argv); exit(1); } else if (pid == -1) { lua_pushliteral(L, "error spawning process (fork error): "); lua_pushstring(L, strerror(errno)); lua_concat(L, 2); goto e_free_argv; } free(argv); lua_newtable(L); luaL_getmetatable(L, "proclib_proc"); lua_setmetatable(L, -2); /* store the pid */ lua_pushliteral(L, "pid"); p = lua_newuserdata(L, sizeof(pid_t)); *p = pid; luaL_getmetatable(L, "proclib_pid"); lua_setmetatable(L, -2); lua_rawset(L, -3); return 1; e_free_argv: free(argv); e_return: /* error string should already be on the stack */ return lua_error(L); } /*! * \brief [lua_CFunction pid:__gc] Kill the process with the given pid. * \param L the lua state to use * * This is the gc function for the pid userdata. This function will send a * TERM signal to the process and then run waitpid. If the process has not * exited after about 1.5 seconds, SIGKILL will be sent. */ static int pid_gc(lua_State *L) { int i = 0; pid_t *p = luaL_checkudata(L, 1, "proclib_pid"); if (kill(*p, SIGTERM)) { return 0; } /* wait for the process to exit, after about 1.5 seconds, send SIGKILL */ while (waitpid(*p, NULL, WNOHANG) == 0) { if (++i == 3) { kill(*p, SIGKILL); continue; } usleep(500); } return 0; } /*! * \brief Convert a timeval struct to milliseconds. * \param tv the timeval struct to operate on * \return the value of the given timeval expressed in milliseconds */ static long tv2ms(struct timeval *tv) { return (tv->tv_sec * 1000 + tv->tv_usec / 1000); } /*! * \brief [lua_CFunction proc:wait] Wait for the process to end. * \param L the lua state to use * \param timeout an optional timeout in milliseconds * * This function calls waitpid on the running process. * * \return a tuple containg the exit code, nil and the signal that caused the * process to exit, or nil and a string describing the error ("error", * "timeout", or "core" currently). */ static int wait_proc(lua_State *L) { pid_t pid; int status; int res; long timeout = luaL_optlong(L, 2, -1); long start; struct timeval tv; luaL_checktype(L, 1, LUA_TTABLE); if (gettimeofday(&tv, NULL)) { lua_pushliteral(L, "error running gettimeofday() for timeout calculations: "); lua_pushstring(L, strerror(errno)); lua_concat(L, 2); return lua_error(L); } start = tv2ms(&tv); /* get the pid of this process */ lua_getfield(L, 1, "pid"); if (lua_isnil(L, -1)) { /* no process found */ lua_pushnil(L); lua_pushliteral(L, "error"); return 2; } pid = *(pid_t *) lua_touserdata(L, -1); lua_pop(L, 1); if (timeout >= 0) { while ((res = waitpid(pid, &status, WNOHANG)) == 0) { /* check the timeout */ if (gettimeofday(&tv, NULL)) { lua_pushliteral(L, "error running gettimeofday() for timeout calculations: "); lua_pushstring(L, strerror(errno)); lua_concat(L, 2); return lua_error(L); } if (timeout < tv2ms(&tv) - start) { lua_pushnil(L); lua_pushliteral(L, "timeout"); return 2; } usleep(1); } if (res == -1) { /* waitpid failed */ lua_pushnil(L); lua_pushliteral(L, "error"); return 2; } } else { if (waitpid(pid, &status, 0) == -1) { /* waitpid failed */ lua_pushnil(L); lua_pushliteral(L, "error"); return 2; } } if (WIFEXITED(status)) { lua_pushinteger(L, WEXITSTATUS(status)); lua_pushnil(L); } else if (WIFSIGNALED(status)) { lua_pushnil(L); if (WCOREDUMP(status)) lua_pushliteral(L, "core"); else lua_pushinteger(L, WTERMSIG(status)); } else { lua_pushliteral(L, "unknown error running waitpid"); return lua_error(L); } /* unset the pid */ lua_pushliteral(L, "pid"); lua_pushnil(L); lua_rawset(L, 1); return 2; } /*! * \brief [lua_CFunction proc:kill] Kill the process with SIGKILL. * \param L the lua state to use * * This function sends SIGKILL then calls proc:wait() and returns the * result. * * \return same as wait_proc() and in the case of "error" an additional error * description and errno may be returned */ static int kill_proc(lua_State *L) { pid_t pid; luaL_checktype(L, 1, LUA_TTABLE); /* get the pid of this process */ lua_getfield(L, 1, "pid"); if (lua_isnil(L, -1)) { /* no process found */ lua_pushnil(L); lua_pushliteral(L, "error"); return 2; } pid = *(pid_t *) lua_touserdata(L, -1); lua_pop(L, 1); if (kill(pid, SIGKILL)) { lua_pushnil(L); lua_pushliteral(L, "error"); lua_pushstring(L, strerror(errno)); lua_pushinteger(L, errno); return 4; } lua_pushcfunction(L, wait_proc); lua_pushvalue(L, 1); lua_call(L, 1, 2); return 2; } /*! * \brief [lua_CFunction proc:term] Terminate process with SIGTERM. * \param L the lua state to use * \param timeout an optional timeout in milliseconds that will be passed to proc:wait() * * This function sends SIGTERM then calls proc:wait() and returns the result. * * \return same as wait_proc() and in the case of "error" an additional error * description and errno may be returned */ static int terminate_proc(lua_State *L) { pid_t pid; long timeout = luaL_optlong(L, 2, -1); luaL_checktype(L, 1, LUA_TTABLE); /* get the pid of this process */ lua_getfield(L, 1, "pid"); if (lua_isnil(L, -1)) { /* no process found */ lua_pushnil(L); lua_pushliteral(L, "error"); return 2; } pid = *(pid_t *) lua_touserdata(L, -1); lua_pop(L, 1); if (kill(pid, SIGTERM)) { lua_pushnil(L); lua_pushliteral(L, "error"); lua_pushstring(L, strerror(errno)); lua_pushinteger(L, errno); return 4; } lua_pushcfunction(L, wait_proc); lua_pushvalue(L, 1); if (timeout == -1) { lua_pushnil(L); } else { lua_pushvalue(L, 2); } lua_call(L, 2, 2); return 2; } static luaL_Reg proclib[] = { {"exists", proc_exists}, {"exec", exec_proc}, {"exec_io", exec_proc_with_stdio}, {NULL, NULL}, }; static luaL_Reg proc_pid[] = { {"__gc", pid_gc}, {NULL, NULL}, }; static luaL_Reg proc_table[] = { {"wait", wait_proc}, {"term", terminate_proc}, {"kill", kill_proc}, {NULL, NULL}, }; /* copied from liolib.c in the lua source */ static int pushresult (lua_State *L, int i, const char *filename) { int en = errno; /* calls to Lua API may change this value */ if (i) { lua_pushboolean(L, 1); return 1; } else { lua_pushnil(L); if (filename) lua_pushfstring(L, "%s: %s", filename, strerror(en)); else lua_pushfstring(L, "%s", strerror(en)); lua_pushinteger(L, en); return 3; } } /* adapted from io_fclose in liolib.c in the lua source */ static int io_fclose (lua_State *L) { FILE **p = luaL_checkudata(L, 1, LUA_FILEHANDLE); int ok = (fclose(*p) == 0); *p = NULL; return pushresult(L, ok, NULL); } int luaopen_proclib(lua_State *L) { /* set up a private environment containing a __close function that is * necessary for using the LUA_FILEHANDLE metatable. This metatable is * used by the proc.exec_io function to manage pipes for stdio. */ lua_createtable(L, 0, 1); lua_replace(L, LUA_ENVIRONINDEX); lua_pushcfunction(L, io_fclose); lua_setfield(L, LUA_ENVIRONINDEX, "__close"); /* register our functions */ luaL_register(L, "proc", proclib); /* set up the pid metatable */ luaL_newmetatable(L, "proclib_pid"); luaL_register(L, NULL, proc_pid); lua_pop(L, 1); /* set up the 'proc' metatable */ luaL_newmetatable(L, "proclib_proc"); /* set the __index value to itself */ lua_pushstring(L, "__index"); lua_pushvalue(L, -2); lua_settable(L, -3); luaL_register(L, NULL, proc_table); /* store this table as 'proc.proc' as well */ lua_setfield(L, -2, "proc"); lua_pop(L, 1); /* load the lua portion of the lib */ if (luaL_loadbuffer(L, proclib_lua, sizeof(proclib_lua), "proclib")) goto e_lua_error; lua_pushstring(L, "proc"); if (lua_pcall(L, 1, 1, 0)) goto e_lua_error; return 1; e_lua_error: /* format the error message a little */ lua_pushstring(L, "error loading the proc library: "); lua_insert(L, -2); lua_concat(L, 2); return lua_error(L); } asterisk-testsuite-0.0.0+svn.5781/asttest/lib/lua/proclib.lua000066400000000000000000000033651242304700500237470ustar00rootroot00000000000000-- -- Asterisk -- An open source telephony toolkit. -- -- Copyright (C) 1999 - 2008, Digium, Inc. -- -- Matthew Nicholson -- -- See http://www.asterisk.org for more information about -- the Asterisk project. Please do not directly contact -- any of the maintainers of this project for assistance; -- the project provides a web site, mailing lists and IRC -- channels for your use. -- -- This program is free software, distributed under the terms of -- the GNU General Public License Version 2. See the LICENSE file -- at the top of the source tree. -- module(..., package.seeall) function exec_and_wait(name, ...) p = exec(name, unpack(arg)) return p:wait() end --- Send the term signal and give the process 10 seconds to exit then kill it. -- @return whatever term() or kill() returned function proc:term_or_kill() local res, err = self:term(10000) if not res and err == 'timeout' then return self:kill() end return res, err end --- Print an appropriate error message for the given error. -- @usage res, err = proc.perror(p:wait()) -- @usage res, err = proc.perror(p:term()) -- @usage res, err = proc.perror(p:kill()) -- @return This function returns its parameters. function perror(res, err, strerror, errno) if res == nil then if err == "core" then print("process crashed (core dumped)") elseif err == "timeout" then return res, err elseif err == "error" then if strerror and errno then print("error running process (" .. strerror .. ")") else print("error running process") end elseif type(err) == "number" then print("process terminated by signal " .. err) else print("error running process (" .. err .. ")") end end return res, err, strerror, errno end asterisk-testsuite-0.0.0+svn.5781/asttest/lib/lua/testlib.c000066400000000000000000000167501242304700500234260ustar00rootroot00000000000000/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 1999 - 2008, Digium, Inc. * * Matthew Nicholson * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ #include #include #include #include "asttest/testsuite.h" #include "testlib_lua.h" /* * \brief Push the current testsuite on the stack and return a pointer to it. * \param L the lua state to use */ static struct testsuite *push_ts(lua_State *L) { lua_getfield(L, LUA_REGISTRYINDEX, "testlib_ts"); return lua_touserdata(L, -1); } /* * \brief Push the current test's name on the stack and return a pointer to it. * \param L the lua state to use */ static const char *push_test_name(lua_State *L) { lua_getfield(L, LUA_REGISTRYINDEX, "testlib_name"); return lua_tostring(L, -1); } /* * \brief Check if the current test was expected to fail or not. * \param L the lua state to use * \note This function expects testlib to be loaded, which should always be the * case by the time it is called. * \return whether the current test was expected to fail or not */ int testlib_expected_fail(lua_State *L) { int res; lua_getfield(L, LUA_REGISTRYINDEX, "testlib_xfail"); res = lua_toboolean(L, -1); lua_pop(L, 1); return res; } /* * \brief Run the atexit functions. * \param L the lua state to use * \param result_index the index where the test results are located * * This function runs all of the atexit functions in the order they were * registered. These functions may change the result of the test. Any errors * that are encountered are logged. * * \note This function expects testlib to be loaded, which should always be the * case by the time it is called. * \warn The old results are removed from the stack when new results are pushed. * \return 0 on success, non zero if new test results have been pushed to the stack. */ int testlib_atexit(lua_State *L, int result_index) { int funcs, res, new_result = 0; struct testsuite *ts; const char *name; lua_getfield(L, LUA_REGISTRYINDEX, "testlib_atexit"); funcs = lua_gettop(L); lua_pushnil(L); while (lua_next(L, funcs)) { lua_pushvalue(L, result_index); if ((res = lua_pcall(L, 1, 0, 0))) { if (lua_isstring(L, -1)) { /* error */ ts = push_ts(L); name = push_test_name(L); ts_log(ts, name, "atexit error: %s\n", lua_tostring(L, -3)); /* name, ts, and the error */ lua_pop(L, 3); } else if (lua_type(L, -1) == LUA_TTABLE) { /* got a new result */ if (new_result) { /* replace the new result */ lua_replace(L, new_result); } else { /* move the new results under 'funcs' in the stack */ lua_insert(L, funcs); new_result = funcs; funcs += 1; } } else { /* got some other error value returned, probably a missing test result */ ts = push_ts(L); name = push_test_name(L); ts_log(ts, name, "atexit error: missing test result\n"); /* name, ts, and the error */ lua_pop(L, 3); } } } /* remove the atexit functions */ lua_pop(L, 1); return new_result; } /* * \brief This function will turn lua errors into result tables and process * unknown results into result tables. * \param L the lua state to use * \note This function expects the result/error to be on the top of the stack. */ void testlib_preprocess_result(lua_State *L) { int result_index = lua_gettop(L); if (lua_isstring(L, -1)) { /* error string */ lua_newtable(L); lua_pushliteral(L, "error"); lua_setfield(L, -2, "result"); lua_pushvalue(L, result_index); lua_setfield(L, -2, "reason"); lua_remove(L, result_index); } else if (lua_type(L, -1) == LUA_TTABLE) { /* do nothing */ } else { /* unknown error type, discard it */ lua_pop(L, 1); lua_newtable(L); lua_pushliteral(L, "error"); lua_setfield(L, -2, "result"); lua_pushliteral(L, "missing test result"); lua_setfield(L, -2, "reason"); } } /* * \brief Push the default test result to the stack (pass is the default). * \param L the lua state to use */ void testlib_default_result(lua_State *L) { lua_newtable(L); lua_pushliteral(L, "pass"); lua_setfield(L, -2, "result"); } /* * \brief [lua_CFunction] Log a message for the current test. * \param message [lua] the string to log */ static int lua_ts_log(lua_State *L) { const char *string = luaL_checkstring(L, 1); struct testsuite *ts = push_ts(L); const char *name = push_test_name(L); ts_log(ts, name, "%s", string); lua_pop(L, 2); /* pop the test name and ts */ return 0; } /* * \brief [lua_CFunction] Notify the test driver that this test is expected to * fail. */ static int lua_xfail(lua_State *L) { lua_pushboolean(L, 1); lua_setfield(L, LUA_REGISTRYINDEX, "testlib_xfail"); return 0; } /* * \brief Add a function to the end of the atexit table. * * Each atexit function that is called is passed a table containing the result * of the current test in the following form. * * \code * table = { * result = "pass" or "fail" or "skip" or "error"; * reason = nil or "reason string"; * } * \endcode * * At exit functions can alter the test result using the same method test * results are returned from a test (pass(), fail(), etc.). All atexit * functions are passed the original test result, not any modified results. * The last modified test result returned will be passed to the testsuite * engine. */ static int lua_atexit(lua_State *L) { luaL_checktype(L, 1, LUA_TFUNCTION); lua_getglobal(L, "table"); if (lua_isnil(L, -1)) { lua_pop(L, 1); return luaL_error(L, "error running table.insert, 'table' lib not found"); } lua_getfield(L, -1, "insert"); if (lua_isnil(L, -1)) { lua_pop(L, 2); return luaL_error(L, "error running table.insert, 'insert' not found in 'table' lib"); } /* remove the 'table' table from the stack */ lua_remove(L, -2); /* call table.insert to add the function to the atexit table */ lua_getfield(L, LUA_REGISTRYINDEX, "testlib_atexit"); lua_pushvalue(L, 1); lua_call(L, 2, 0); return 0; } static luaL_Reg testlib[] = { {"log", lua_ts_log}, {"xfail", lua_xfail}, {"atexit", lua_atexit}, {NULL, NULL}, }; int luaopen_testlib(lua_State *L) { const char *test_name; struct testsuite *ts; luaL_checktype(L, 1, LUA_TLIGHTUSERDATA); ts = lua_touserdata(L, 1); test_name = luaL_checkstring(L, 2); /* set up atexit table */ lua_newtable(L); lua_setfield(L, LUA_REGISTRYINDEX, "testlib_atexit"); /* set up some registry values */ lua_pushlightuserdata(L, ts); lua_setfield(L, LUA_REGISTRYINDEX, "testlib_ts"); lua_pushstring(L, test_name); lua_setfield(L, LUA_REGISTRYINDEX, "testlib_name"); lua_pushboolean(L, 0); lua_setfield(L, LUA_REGISTRYINDEX, "testlib_xfail"); /* register our functions */ luaL_register(L, "test", testlib); lua_pushstring(L, test_name); lua_setfield(L, -2, "name"); lua_pop(L, 1); /* load the lua portion of the lib */ if (luaL_loadbuffer(L, testlib_lua, sizeof(testlib_lua), "testlib")) goto e_lua_error; lua_pushstring(L, "test"); if (lua_pcall(L, 1, 1, 0)) goto e_lua_error; return 1; e_lua_error: /* format the error message a little */ lua_pushstring(L, "error loading test library: "); lua_insert(L, -2); lua_concat(L, 2); return lua_error(L); } asterisk-testsuite-0.0.0+svn.5781/asttest/lib/lua/testlib.lua000066400000000000000000000037301242304700500237570ustar00rootroot00000000000000-- -- Asterisk -- An open source telephony toolkit. -- -- Copyright (C) 1999 - 2008, Digium, Inc. -- -- Matthew Nicholson -- -- See http://www.asterisk.org for more information about -- the Asterisk project. Please do not directly contact -- any of the maintainers of this project for assistance; -- the project provides a web site, mailing lists and IRC -- channels for your use. -- -- This program is free software, distributed under the terms of -- the GNU General Public License Version 2. See the LICENSE file -- at the top of the source tree. -- module(..., package.seeall) -- -- replacements for global functions -- -- print to the test log instead of stdout function _G.print(...) local msg = "" for i=1,select('#', ...) do if i == 1 then msg = msg .. tostring(select(i, ...)) else msg = msg .. "\t" .. tostring(select(i, ...)) end end msg = msg .. "\n" log(msg) end _G.lua_error = _G.error _G.xfail = xfail _G.atexit = atexit -- -- basic pass/fail/skip/error functions -- note: none of these functions actually return -- function pass(reason) return lua_error{result = "pass", reason = reason} end _G.pass = pass function fail(reason) return lua_error{result = "fail", reason = reason} end _G.fail = fail function skip(reason) return lua_error{result = "skip", reason = reason} end _G.skip = skip function error(reason) return lua_error{result = "error", reason = reason} end _G.error = error -- -- utility functions -- -- skip if the given condition is met function skip_if(condition, message) if condition then return skip(message) end end _G.skip_if = skip_if -- fail if the given condition is met function fail_if(condition, message) if condition then return fail(message) end end _G.fail_if = fail_if -- check the return value of a function printing the given message in the event -- of an error function check(message, r, err) if not r then error(message .. ": " .. tostring(err)) end return r, err end _G.check = check asterisk-testsuite-0.0.0+svn.5781/asttest/lib/testsuite.c000066400000000000000000000105451242304700500232240ustar00rootroot00000000000000/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 1999 - 2008, Digium, Inc. * * Matthew Nicholson * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ #include #include #include #include #include #include #include "asttest/asttest.h" #include "asttest/testsuite.h" int ts_init(struct testsuite *ts, const char *path, struct asttest_opts *opts) { char log_path[PATH_MAX]; char cwd[PATH_MAX]; memset(ts, 0, sizeof(struct testsuite)); ts->asterisk_version = opts->asterisk_version; snprintf(log_path, sizeof(log_path), "%s/%s", path, opts->log_filename); ts->log = fopen(log_path, "w"); if (!ts->log) { fprintf(stderr, "Error opening log file for writing (%s): %s\n", log_path, strerror(errno)); goto e_return; } /* make asterisk_path absolute */ if (opts->asterisk_path[0] == '/') { /* path starts with '/' we will assume it is absolute */ snprintf(ts->asterisk_path, sizeof(ts->asterisk_path), "%s", opts->asterisk_path); } else { if (!getcwd(cwd, sizeof(cwd))) { fprintf(stderr, "Error determining the current working directory\n"); goto e_close_log; } snprintf(ts->asterisk_path, sizeof(ts->asterisk_path), "%s/%s", cwd, opts->asterisk_path); } return 0; e_close_log: fclose(ts->log); e_return: return 1; } int ts_init_single(struct testsuite *ts, struct asttest_opts *opts) { char cwd[PATH_MAX]; memset(ts, 0, sizeof(struct testsuite)); ts->log = stdout; ts->single_test_mode = 1; ts->asterisk_version = opts->asterisk_version; /* make asterisk_path absolute */ if (opts->asterisk_path[0] == '/') { /* path starts with '/' we will assume it is absolute */ snprintf(ts->asterisk_path, sizeof(ts->asterisk_path), "%s", opts->asterisk_path); } else { if (!getcwd(cwd, sizeof(cwd))) { printf("Error determining the current working directory\n"); goto e_return; } snprintf(ts->asterisk_path, sizeof(ts->asterisk_path), "%s/%s", cwd, opts->asterisk_path); } return 0; e_return: return 1; } void ts_cleanup(struct testsuite *ts) { if (ts->log && ts->log != stdout) { fclose(ts->log); } } void ts_print(struct testsuite *ts) { printf("Test results:\n"); printf(" tests passed: %d\n", ts->pass); printf(" tests failed: %d\n", ts->fail); printf(" unexpected passes: %d\n", ts->xpass); printf(" expected failures: %d\n", ts->xfail); printf(" tests skipped: %d\n", ts->skip); printf(" test errors: %d\n", ts->error); printf("\n"); printf("Total tests run: %d\n", ts->total); } int ts_log_va(struct testsuite *ts, const char *test_name, const char *fmt, va_list ap) { int res = 0; if (!ts->single_test_mode) { res = fprintf(ts->log, "%s: ", test_name); } res += vfprintf(ts->log, fmt, ap); fflush(ts->log); return res; } int __attribute__((format(printf, 3, 4))) ts_log(struct testsuite *ts, const char *test_name, const char *fmt, ...) { va_list ap; int res; va_start(ap, fmt); res = ts_log_va(ts, test_name, fmt, ap); va_end(ap); return res; } enum ts_result ts_pass(struct testsuite *ts, const char *test_name) { ts->pass++; ts->total++; ts_log(ts, test_name, "test passed\n"); return TS_PASS; } enum ts_result ts_fail(struct testsuite *ts, const char *test_name) { ts->fail++; ts->total++; ts_log(ts, test_name, "test failed\n"); return TS_FAIL; } enum ts_result ts_xpass(struct testsuite *ts, const char *test_name) { ts->xpass++; ts->total++; ts_log(ts, test_name, "unexpected pass\n"); return TS_XPASS; } enum ts_result ts_xfail(struct testsuite *ts, const char *test_name) { ts->xfail++; ts->total++; ts_log(ts, test_name, "expected failure\n"); return TS_XFAIL; } enum ts_result ts_skip(struct testsuite *ts, const char *test_name) { ts->skip++; ts->total++; ts_log(ts, test_name, "test skipped\n"); return TS_SKIP; } enum ts_result ts_error(struct testsuite *ts, const char *test_name) { ts->error++; ts->total++; ts_log(ts, test_name, "error running test\n"); return TS_ERROR; } asterisk-testsuite-0.0.0+svn.5781/asttest/lib/testutils.c000066400000000000000000000176441242304700500232420ustar00rootroot00000000000000/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 1999 - 2008, Digium, Inc. * * Matthew Nicholson * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "asttest/asttest.h" #include "asttest/lua.h" #include "asttest/testsuite.h" #include "asttest/testutils.h" /* * \brief Check if the given result string equals the string stored at the * given index. * \param L the lua state to use * \param result_index the index of the result string * \param result_string the result string to check for equality with the string * at the given index */ static int result_equals(lua_State *L, int result_index, const char *result_string) { int res; lua_pushstring(L, result_string); res = lua_equal(L, -1, result_index); lua_pop(L, 1); return res; } /* * \brief Process the result of a test. * \param ts the current test suite * \param L the lua state the test was run in * * This function expects to be called after luaL_dofile/lua_pcall and will * analyze the result of running the test. The testing framework will pass a * special table containg the test result to lua_error() that is used by this * function. * * The table is expected in the following format: * * \code * table = { * result = "pass" or "fail" or "skip" or "error"; * reason = nil or "reason string"; * } * \endcode */ static enum ts_result process_test_result(struct testsuite *ts, const char *test_name, lua_State *L) { enum ts_result res = TS_ERROR; testlib_preprocess_result(L); if (lua_type(L, -1) == LUA_TTABLE) { int result_table = lua_gettop(L); int reason_string = 0; int test_result = 0; lua_getfield(L, result_table, "reason"); if (!lua_isnil(L, -1) && lua_isstring(L, -1)) reason_string = lua_gettop(L); else lua_pop(L, 1); lua_getfield(L, result_table, "result"); if (lua_isnil(L, -1)) { lua_pop(L, 1); ts_log(ts, test_name, "error reading test result\n"); res = ts_error(ts, test_name); } else { test_result = lua_gettop(L); if (reason_string) ts_log(ts, test_name, "%s\n", lua_tostring(L, reason_string)); if (result_equals(L, test_result, "pass")) { if (testlib_expected_fail(L)) res = ts_xpass(ts, test_name); else res = ts_pass(ts, test_name); } else if (result_equals(L, test_result, "fail")) { if (testlib_expected_fail(L)) res = ts_xfail(ts, test_name); else res = ts_fail(ts, test_name); } else if (result_equals(L, test_result, "skip")) { res = ts_skip(ts, test_name); } else if (result_equals(L, test_result, "error")) { res = ts_error(ts, test_name); } else { ts_log(ts, test_name, "unknown result '%s'\n", lua_tostring(L, test_result)); res = ts_error(ts, test_name); } } if (test_result) lua_remove(L, test_result); if (reason_string) lua_remove(L, reason_string); lua_pop(L, 1); } else { /* this should never happen, testlib_preprocess_result() should * convert any values to proper result tables for us */ lua_pop(L, 1); ts_log(ts, test_name, "missing test result\n"); res = ts_error(ts, test_name); } return res; } static void print_test_name(struct testsuite *ts, const char *test_name) { int len, i; /* first print a number */ len = printf("%d.", ts->total + 1); /* pad the number printed */ for (i = 4 - len; i > 0; i--) { printf(" "); } /* now print the test name */ len = printf(" %s ", test_name); /* now pad the test name */ for (i = 31 - len; i > 0; i--) { printf(" "); } fflush(stdout); } static void print_test_result(enum ts_result result) { switch (result) { case TS_PASS: printf("pass\n"); break; case TS_FAIL: printf("fail\n"); break; case TS_XPASS: printf("xpass\n"); break; case TS_XFAIL: printf("xfail\n"); break; case TS_SKIP: printf("skip\n"); break; case TS_ERROR: printf("error\n"); break; default: printf("unknown\n"); break; } fflush(stdout); } static enum ts_result run_test(struct testsuite *ts, const char *test_name, const char *test_dir_path) { lua_State *L; char original_path[PATH_MAX]; enum ts_result result; int result_index; if (!getcwd(original_path, PATH_MAX)) { ts_log(ts, test_name, "internal error storing current path, PATH_MAX is too small\n"); return ts_error(ts, test_name); } if (chdir(test_dir_path)) { ts_log(ts, test_name, "error changing to test dir: %s\n", strerror(errno)); return ts_error(ts, test_name); } if (!(L = get_lua_state(ts, test_name))) { ts_log(ts, test_name, "internal error, cannot run test\n"); if (chdir(original_path)) ts_log(ts, test_name, "additionaly, there was an error changing directories, this may cause further errors (%s)\n", strerror(errno)); return ts_error(ts, test_name); } if (!luaL_dofile(L, "test.lua")) { /* we got no explicit result, consider it a pass for now */ testlib_default_result(L); } /* run our atexit functions */ testlib_preprocess_result(L); result_index = lua_gettop(L); if (testlib_atexit(L, result_index)) { lua_remove(L, result_index); } /* process the test result */ result = process_test_result(ts, test_name, L); if (chdir(original_path)) ts_log(ts, test_name, "error changing directories, this may cause further errors (%s)\n", strerror(errno)); lua_close(L); return result; } static int is_directory(const char *dir) { struct stat st; if (lstat(dir, &st)) { return 0; } return S_ISDIR(st.st_mode); } static int ignored_dir(struct testsuite *ts, const char *test_name, const char *full_path) { if (!test_name || strlen(test_name) < 1) { return 1; } if (!is_directory(full_path)) { return 1; } if (test_name[0] == '.') { /* skip hidden directories */ return 1; } else if (test_name[0] == '_') { /* skip directories starting with '_' and log the skippage */ ts_log(ts, test_name, "automatically skipping test because test name starts with '_'\n"); print_test_name(ts, test_name); print_test_result(ts_skip(ts, test_name)); return 1; } return 0; } int process_single_test(struct asttest_opts *opts) { struct testsuite ts; int res = 0; if (ts_init_single(&ts, opts)) { printf("Error running test\n"); return 1; } run_test(&ts, opts->single_test_mode, opts->single_test_mode); if (ts.fail || ts.xpass || ts.error) { res = 1; } ts_cleanup(&ts); return res; } int process_test_dir(const char *path, struct asttest_opts *opts) { DIR *main_dir = opendir(path); char full_path[PATH_MAX]; struct testsuite ts; struct dirent *ent; enum ts_result result; int res = 0; printf("Processing tests in '%s':\n", path); if (!main_dir) { fprintf(stderr, "Error opening path '%s': %s\n", path, strerror(errno)); return 1; } ts_init(&ts, path, opts); while ((ent = readdir(main_dir))) { snprintf(full_path, sizeof(full_path), "%s/%s", path, ent->d_name); if (!ignored_dir(&ts, ent->d_name, full_path)) { print_test_name(&ts, ent->d_name); result = run_test(&ts, ent->d_name, full_path); print_test_result(result); } } closedir(main_dir); printf("\n"); ts_print(&ts); /* consider this run a failure if any tests failed or passed * unexpectedly */ if (ts.fail || ts.xpass) res = 1; if (opts->warn_on_error && ts.error) { printf("\n***WARNING: some tests failed to run, see log for details\n"); } else if (ts.error) { /* signal a failure */ res = 1; } ts_cleanup(&ts); return res; } asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/000077500000000000000000000000001242304700500223455ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/asterisk_cli/000077500000000000000000000000001242304700500250215ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/asterisk_cli/test.lua000066400000000000000000000010761242304700500265070ustar00rootroot00000000000000-- test spawning asterisk skip_if(not ast.exists(), "asterisk not found") a = ast.new() a:spawn() version, err = a:cli("core show version") fail_if(not version, "error running asterisk -rx 'core show version': " .. tostring(err)) print(version) res, err = proc.perror(a:term_or_kill()) if res == nil then fail("error running asterisk") elseif res ~= 0 then fail("error, asterisk exited with status " .. res) end fail_if(a:cli("core show version"), "some how 'core show version' succeeded when asterisk was not running") pass("asterisk exited with status " .. res) asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/asterisk_version/000077500000000000000000000000001242304700500257375ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/asterisk_version/test.lua000066400000000000000000000171541242304700500274310ustar00rootroot00000000000000-- test asterisk version string parsing function normal_version(version, concept, major, minor, patch) print("testing " .. version) local v = ast.version(version) fail_if(v.svn, version .. " was detected as an svn version") fail_if(tostring(v) ~= version, string.format("tostring(v) for %s ~= %s", version, version)) fail_if(v.concept ~= concept, string.format("v.concept ~= concept (%s ~= %s)", v.concept or "nil", concept or "nil")) fail_if(v.major ~= major, string.format("v.major ~= major (%s ~= %s)", v.major or "nil", major or "nil")) fail_if(v.minor ~= minor, string.format("v.minor ~= minor (%s ~= %s)", v.minor or "nil", minor or "nil")) fail_if(v.patch ~= patch, string.format("v.patch ~= patch (%s ~= %s)", v.patch or "nil", patch or "nil")) end function synthetic_version(version, concept, major, minor, patch) print("testing synthetic " .. version) local v = ast.version(version) fail_if(not v.svn, version .. " was NOT detected as an svn version") fail_if(tostring(v) ~= version, string.format("tostring(v) for %s ~= %s", version, version)) fail_if(v.concept ~= concept, string.format("v.concept ~= concept (%s ~= %s)", v.concept or "nil", concept or "nil")) fail_if(v.major ~= major, string.format("v.major ~= major (%s ~= %s)", v.major or "nil", major or "nil")) fail_if(v.minor ~= minor, string.format("v.minor ~= minor (%s ~= %s)", v.minor or "nil", minor or "nil")) fail_if(v.patch ~= patch, string.format("v.patch ~= patch (%s ~= %s)", v.patch or "nil", patch or "nil")) end function svn_version(version, branch, revision, parent) print("testing svn " .. version) local v = ast.version(version) fail_if(not v.svn, version .. " was NOT detected as an svn version") fail_if(tostring(v) ~= version, string.format("tostring(v) for %s ~= %s", version, version)) fail_if(v.branch ~= branch, string.format("v.branch ~= branch (%s ~= %s)", v.branch or "nil", branch or "nil")) fail_if(v.revision ~= revision, string.format("v.revision ~= revision (%s ~= %s)", v.revision or "nil", revision or "nil")) fail_if(v.parent ~= parent, string.format("v.parent ~= parent (%s ~= %s)", v.parent or "nil", parent or "nil")) end function synthetic_svn_version(version, branch, revision) print("testing synthetic svn " .. version) local v = ast.version(version) fail_if(v.svn, version .. " was detected as an svn version") fail_if(tostring(v) ~= version, string.format("tostring(v) for %s ~= %s", version, version)) fail_if(v.branch ~= branch, string.format("v.branch ~= branch (%s ~= %s)", v.branch or "nil", branch or "nil")) fail_if(v.revision ~= revision, string.format("v.revision ~= revision (%s ~= %s)", v.revision or "nil", revision or "nil")) end function major_version(v1, v2) print(string.format("testing major version %s matches %s", v1, v2)) local old_version = ast._version ast._version = function() return v2 end fail_if(not ast.has_major_version(v1), string.format("ast.has_major_version(%s) failed for %s", v1, v2)) ast._version = old_version end function not_major_version(v1, v2) print(string.format("testing major version %s does not match %s", v1, v2)) local old_version = ast._version ast._version = function() return v2 end fail_if(ast.has_major_version(v1), string.format("ast.has_major_version(%s) matched for %s", v1, v2)) ast._version = old_version end function compare_false(str) local left, cmp, right = str:match("^([^%s]+) ([>=<~]+) ([^%s]+)$") if not left then error(string.format("invalid comparison string '%s'", str)) end local chunk_str = string.format("return ast.version(\"%s\") %s ast.version(\"%s\")", left, cmp, right) local chunk, err = loadstring(chunk_str, "compairson") if not chunk then error(string.format("error building compairson for '%s' (%s): %s", str, chunk_str, err)) end fail_if(chunk(), string.format("%s was true, should have been false (%s)", str, chunk_str)) end function compare(str) local left, cmp, right = str:match("^([^%s]+) ([>=<~]+) ([^%s]+)$") if not left then error(string.format("invalid comparison string '%s'", str)) end local chunk_str = string.format("return not (ast.version(\"%s\") %s ast.version(\"%s\"))", left, cmp, right) local chunk, err = loadstring(chunk_str, "comparison") if not chunk then error(string.format("error building compairson for '%s' (%s): %s", str, chunk_str, err)) end fail_if(chunk(), string.format("%s failed (%s)", str, chunk_str)) end normal_version("1.4.30", "1", "4", "30") normal_version("1.4.30.1", "1", "4", "30", "1") normal_version("1.4", "1", "4") normal_version("C.3", "C", "3") normal_version("C.3.5", "C", "3", "5") normal_version("10", "10") normal_version("10.0", "10", "0") normal_version("10.0.1", "10", "0", "1") synthetic_svn_version("1.4.30", "branch-1.4", "00000") synthetic_svn_version("1.4.30.1", "branch-1.4", "00000") synthetic_svn_version("1.4", "branch-1.4", "00000") synthetic_svn_version("C.3", "branch-C.3", "00000") synthetic_svn_version("C.3.5", "branch-C.3", "00000") synthetic_svn_version("C.3", "branch-C.3", "00000") synthetic_svn_version("10", "branch-10", "00000") synthetic_svn_version("10.0", "branch-10", "00000") synthetic_svn_version("10.0.1", "branch-10", "00000") svn_version("SVN-trunk-r252849", "trunk", "252849") svn_version("SVN-branch-1.6.2-r245581M", "branch-1.6.2", "245581M") svn_version("SVN-branch-C.3-r1234", "branch-C.3", "1234") svn_version("SVN-branch-10-r1234", "branch-10", "1234") svn_version("SVN-branch-C.3-duckpond-r1234", "branch-C.3-duckpond", "1234") svn_version("SVN-russell-cdr-q-r249059M-/trunk", "russell-cdr-q", "249059M", "/trunk") svn_version("SVN-russell-rest-r1234", "russell-rest", "1234") synthetic_version("SVN-trunk-r252849", "999", "0", "0", "252849") synthetic_version("SVN-branch-1.6.2-r245581M", "1", "6", "2", "245581") synthetic_version("SVN-branch-10-r245581M", "10", "245581") synthetic_version("SVN-russell-cdr-q-r249059M-/trunk", "998", "0", "0", "249059") synthetic_version("SVN-russell-rest-r1234", "998", "0", "0", "1234") synthetic_version("SVN-branch-1.4-r1234", "1", "4", "999", "1234") synthetic_version("SVN-branch-C.3-r1234", "C", "3", "998", "1234") synthetic_version("SVN-branch-C.3-duckpond-r1234", "C", "3", "999", "1234") major_version("1.4", "1.4.30") major_version("1.4", "1.4.30.1") major_version("trunk", "SVN-trunk-r224353") major_version("1.4", "SVN-branch-1.4-r224353") major_version("1.6.2", "SVN-branch-1.6.2-r224353") major_version("1.6.2", "1.6.2.0") major_version("C.3", "C.3") major_version("C.3", "C.3.5") major_version("10", "10.0") not_major_version("1.4", "1.6.2") not_major_version("1.4", "1.8") not_major_version("1.6", "SVN-trunk-r224353") not_major_version("1.6.1", "1.6.2") not_major_version("trunk", "1.6.2") not_major_version("trunk", "C.3") not_major_version("C.3", "1.4.3") print("testing comparisons") -- these comparisons should evaluate to false compare_false("1.6 > 1.6.2") compare_false("1.6 == 1.6.2") compare_false("1.6.2 > 1.6.2") compare_false("1.6.2 < 1.6.2") compare_false("1.6.2 ~= 1.6.2") compare_false("1.6.2 >= 1.6.3") -- these comparisons should evaluate to true compare("10 > 1.6.2") compare("10.0 > 1.6.2") compare("1.6.2 <= 1.6.2") compare("1.6.2 <= 1.6.3") compare("1.4 < 1.6.2") compare("1.4 < 1.4.2") compare("1.4.30 < 1.6") compare("1.4.30 < SVN-branch-1.6.2-r224353") compare("1.4.30 < SVN-branch-1.4-r224353") compare("SVN-branch-1.6.2-r224353 == SVN-branch-1.6.2-r224353") compare("SVN-branch-1.6.2-r224352 < SVN-branch-1.6.2-r224353") compare("SVN-trunk-r1234 > SVN-branch-1.6.2-r224353") compare("C.3 < 1.6.2") compare("C.3 > 1.4.34") compare("C.3.6 > 1.4.34") compare("C.3.6.2 > 1.4.34") compare("C.3.6.2 > C.3") compare("C.3.6.2 > C.3.6") if ast.exists() then print("automatically detected version " .. tostring(ast.version())) end asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/astlib_configs/000077500000000000000000000000001242304700500253335ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/astlib_configs/test.lua000066400000000000000000000030641242304700500270200ustar00rootroot00000000000000-- test astlib skip_if(not ast.exists(), "asterisk not found") -- generate what we expect to see expected_test_conf = [[ [test] test = 1 test = test value = this allow = true [section] value = yes setting = on ip = 10.10.10.10 [section] value = no #include file [template](!) value = default [inherit](template) value = default [inherit](template,test) value = default [inherit_template](!,template,test) value = default ]] f = io.open("expected_test.conf", "w") f:write(expected_test_conf) f:close() -- generate a config c = ast.config:new("test.conf") s = c:new_section("test") s["test"] = 1 s["test"] = "test" s["value"] = "this" s["allow"] = true s = c:new_section("section") s["value"] = "yes" s["setting"] = "on" s["ip"] = "10.10.10.10" s = c:new_section("section") s["value"] = "no" c:verbatim("#include file\n\n") s = c:new_section("template") s.template = true s["value"] = "default" s = c:new_section("inherit") s.inherit = {"template"} s["value"] = "default" s = c:new_section("inherit") s.inherit = {"template", "test"} s["value"] = "default" s = c:new_section("inherit_template") s.template = true s.inherit = {"template", "test"} s["value"] = "default" c:_write() f = io.open("test.conf") test_conf = f:read("*a") f:close() -- diff the two os.execute("diff -u expected_test.conf test.conf > diff") f = io.open("diff") diff = f:read("*a") f:close() -- cleanup os.execute("rm -f test.conf expected_test.conf diff") -- check if our two configs match fail_if(test_conf ~= expected_test_conf, "test_conf does not match expected_test_conf\n\n" .. diff) asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/astlib_configs2/000077500000000000000000000000001242304700500254155ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/astlib_configs2/test.lua000066400000000000000000000035331242304700500271030ustar00rootroot00000000000000-- test astlib skip_if(not ast.exists(), "asterisk not found") -- generate what we expect to see expected_test_conf = [[ [test] test = 1 test = test value = this allow = true [section] value = yes setting = on ip = 10.10.10.10 [section] value = no #include file [template](!) value = default [inherit](template) value = default [inherit](template,test) value = default [inherit_template](!,template,test) value = default ]] f = io.open("expected_test.conf", "w") f:write(expected_test_conf) f:close() -- generate a config a = ast.new() a["test.conf"]["test"]["test"] = 1 a["test.conf"]["test"]["test"] = "test" a["test.conf"]["test"]["value"] = "this" a["test.conf"]["test"]["allow"] = true a["test.conf"]["section"]["value"] = "yes" a["test.conf"]["section"]["setting"] = "on" a["test.conf"]["section"]["ip"] = "10.10.10.10" s = a["test.conf"]:new_section("section") s["value"] = "no" a["test.conf"]:verbatim("#include file\n\n") a["test.conf"]["template"].template = true a["test.conf"]["template"]["value"] = "default" a["test.conf"]["inherit"].inherit = {"template"} a["test.conf"]["inherit"]["value"] = "default" s = a["test.conf"]:new_section("inherit") s.inherit = {"template", "test"} s["value"] = "default" a["test.conf"]["inherit_template"].template = true a["test.conf"]["inherit_template"].inherit = {"template", "test"} a["test.conf"]["inherit_template"]["value"] = "default" a:spawn() a:term_or_kill() f = io.open(a:path("/etc/asterisk/test.conf")) test_conf = f:read("*a") f:close() -- diff the two os.execute("diff -u expected_test.conf " .. a:path("/etc/asterisk/test.conf") .. " > diff") f = io.open("diff") diff = f:read("*a") f:close() -- cleanup os.execute("rm -f test.conf expected_test.conf diff") -- check if our two configs match fail_if(test_conf ~= expected_test_conf, "test_conf does not match expected_test_conf\n\n" .. diff) asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/astlib_load_config/000077500000000000000000000000001242304700500261475ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/astlib_load_config/test.lua000066400000000000000000000015271242304700500276360ustar00rootroot00000000000000-- test astlib skip_if(not ast.exists(), "asterisk not found") -- generate what we expect to see expected_test_conf = [[ [test] test = 1 test = test value = this allow = true ]] f = io.open("expected_test.conf", "w") f:write(expected_test_conf) f:close() -- load what we just wrote a = ast.new() a:load_config("expected_test.conf") a:spawn() a:term_or_kill() test_conf_path = a.work_area .. "/etc/asterisk/expected_test.conf" f = io.open(test_conf_path) test_conf = f:read("*a") f:close() -- diff the two os.execute("diff -u expected_test.conf " .. test_conf_path .. " > diff") f = io.open("diff") diff = f:read("*a") f:close() -- cleanup os.execute("rm -f " .. test_conf_path .. " expected_test.conf diff") -- check if our two configs match fail_if(test_conf ~= expected_test_conf, "test_conf does not match expected_test_conf\n\n" .. diff) asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/astlib_load_config_in_dir/000077500000000000000000000000001242304700500274735ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/astlib_load_config_in_dir/test.lua000066400000000000000000000015471242304700500311640ustar00rootroot00000000000000-- test astlib skip_if(not ast.exists(), "asterisk not found") -- generate what we expect to see expected_test_conf = [[ [test] test = 1 test = test value = this allow = true ]] f = io.open("dir/expected_test.conf", "w") f:write(expected_test_conf) f:close() -- load what we just wrote a = ast.new() a:load_config("dir/expected_test.conf") a:spawn() a:term_or_kill() test_conf_path = a.work_area .. "/etc/asterisk/expected_test.conf" f = io.open(test_conf_path) test_conf = f:read("*a") f:close() -- diff the two os.execute("diff -u dir/expected_test.conf " .. test_conf_path .. " > diff") f = io.open("diff") diff = f:read("*a") f:close() -- cleanup os.execute("rm -f " .. test_conf_path .. " dir/expected_test.conf diff") -- check if our two configs match fail_if(test_conf ~= expected_test_conf, "test_conf does not match expected_test_conf\n\n" .. diff) asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/astlib_manager/000077500000000000000000000000001242304700500253155ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/astlib_manager/test.lua000066400000000000000000000011021242304700500267710ustar00rootroot00000000000000-- test spawning and connecting to asterisk skip_if(not ast.exists(), "asterisk not found") a = ast.new() a:generate_manager_conf() a:spawn() m, err = a:manager_connect() if not m then fail("error connecting to asterisk: " .. err) end action = ast.manager.action r, err = m(action.login()) if not r then error("error logging in to the manager: " .. err) end if r["Response"] ~= "Success" then fail("error authenticating: " .. r["Message"]) end r = m(action.logoff()) status = proc.perror(a:term_or_kill()) if not status then fail("error terminating asterisk") end asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/cdrlib/000077500000000000000000000000001242304700500236045ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/cdrlib/buggy.csv000066400000000000000000000003061242304700500254350ustar00rootroot00000000000000"","test","100","context","""Test User"" ","SIP/test-02897fd8","","Hangup","","2009-01-14 23:51:15","2009-01-14 23:51:15","2009-01-14 23:51:20",5,5,"ANSWERED","DOCUMENTATION","1231977075.0"," asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/cdrlib/good.csv000066400000000000000000000017361242304700500252600ustar00rootroot00000000000000"","test","100","context","""Test User"" ","SIP/test-02897fd8","","Hangup","","2009-01-14 23:51:15","2009-01-14 23:51:15","2009-01-14 23:51:20",5,5,"ANSWERED","DOCUMENTATION","1231977075.0","" "","test","100","context","""Test User"" ","SIP/test-02897fd8","","Hangup","","2009-01-14 23:51:15","2009-01-14 23:51:15","2009-01-14 23:51:20",5,7,"ANSWERED","DOCUMENTATION","1231977075.0","" "","test","100","context","""Test User"" ","SIP/test-02897fd8","","Hangup","","2009-01-14 23:51:15","2009-01-14 23:51:15","2009-01-14 23:51:20",5,0,"FAILED","DOCUMENTATION","1231977075.0","" "","test","100","context","""Test User"" ","SIP/test-02897fd8","","Hangup","","2009-01-14 23:51:15","2009-01-14 23:51:15","2009-01-14 23:51:20",5,0,"NO ANSWER","DOCUMENTATION","1231977075.0","" "","test","100","context","""Test User"" ","SIP/test-02897fd8","","Hangup","","2009-01-14 23:51:15","2009-01-14 23:51:15","2009-01-14 23:51:20",5,0,"BUSY","DOCUMENTATION","1231977075.0","" asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/cdrlib/test.lua000066400000000000000000000021371242304700500252710ustar00rootroot00000000000000 require "cdr" -- imitate an asterisk object and the asterisk:path() function function a(file) local a = {} function a:path() return file end return a end function check_record(c, record, field, value) fail_if(c[record][field] ~= value, ("%s of record %s is '%s' instead of '%s'"):format(field, record, tostring(c[record][field]), tostring(value))) end print("parsing a known good csv file") c, err = cdr.new(a("good.csv")) fail_if(not c, "failed to parse known good csv file, good.csv: " .. tostring(err)) fail_if(c:len() ~= 5, "cdr file contains 5 records, but the parser found " .. c:len() .. " records") check_record(c, 1, "disposition", "ANSWERED") check_record(c, 2, "billsec", "7") print("parsing a csv file with errors") c, err = cdr.new(a("buggy.csv")) fail_if(c, "successfully parsed known buggy csv file, buggy.csv. This should have failed.") print("attempting to parse a non existent file") c, err = cdr.new(a("does not exits.csv")) fail_if(c, "successfully parsed non existent file???") fail_if(ast.cdr ~= cdr, "ast.cdr ~= cdr, the cdr lib is not properly being inserted into the ast lib") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/proclib_exists/000077500000000000000000000000001242304700500253765ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/proclib_exists/test.lua000066400000000000000000000013001242304700500270520ustar00rootroot00000000000000-- test proclib's ablity to detect missing and non-executable files -- this function expects the give path to exist function should_succeed(path) local res, err = proc.exists(path) if not res then fail("error detecting '" .. path .. "': " .. err) end end -- this function expects the given path to NOT exist function should_fail(path) local res, err = proc.exists(path) if res then fail("falsely detected '" .. path .. "'") end end should_succeed("echo") should_succeed("/bin/echo") should_succeed("/bin/sh") should_fail("this file won't exist, except for under extreme circumstances") should_fail("/same for this one") should_fail("") should_fail("/tmp") should_fail("/var/log/syslog") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/proclib_io/000077500000000000000000000000001242304700500244665ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/proclib_io/test.lua000066400000000000000000000014471242304700500261560ustar00rootroot00000000000000-- test proclib's ablity to read from stdout and stderr and write to stdin print("reading from stdout") test_string = "this is a test" p = proc.exec_io("echo", "-n", test_string) res = p.stdout:read("*a") fail_if(test_string ~= res, "echo test failed: read '" .. res .. "' expected '" .. test_string .. "'") print("writing to stdin and reading from stdout") p = proc.exec_io("cat") p.stdin:write(test_string) p.stdin:close() res = p.stdout:read("*a") fail_if(test_string ~= res, "cat test failed: read '" .. res .. "' expected '" .. test_string .. "'") print("reading from stderr") p = proc.exec_io("cat", "this path should not exist") res = p.stderr:read("*a") print("read: " .. tostring(res)) fail_if(not res, "error reading from stderr") fail_if(#res == 0, "result was a zero length string") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/proclib_perror/000077500000000000000000000000001242304700500253705ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/proclib_perror/test.lua000066400000000000000000000032241242304700500270530ustar00rootroot00000000000000-- test proclib's ablity to detect missing and non-executable files real_print = print function print(msg) printed = true if msg ~= expected_msg then print = real_print fail("expected perror to print '" .. tostring(expected_msg) .. "' but got '" .. msg .. "' instead") end end function expect(msg) printed = false expected_msg = msg end function check() if not printed then print = real_print fail("expected perror to print '" .. tostring(expected_msg) .. "' but got nothing instead") end expected_msg = nil printed = false end function test_crash() real_print("testing crash") expect("process crashed (core dumped)") proc.perror(nil, "core") check() end function test_timeout() real_print("testing a timeout") expect(nil) local p = proc.exec("sleep", "1") proc.perror(p:wait(1)) end function test_error_1() real_print("testing an error (1)") expect("error running process") proc.perror(nil, "error") check() end function test_error_2() real_print("testing an error (2)") expect("error running process (error)") proc.perror(nil, "error", "error", 1) check() end function test_signal_1() real_print("testing a signal (1)") expect("process terminated by signal 0") proc.perror(nil, 0) check() end function test_signal_2() real_print("testing a signal (2)") expect("process terminated by signal 15") local p = proc.exec("sleep", "1") proc.perror(p:term()) check() end function test_unknown() real_print("testing an unknown error (2)") expect("error running process (unknown)") proc.perror(nil, "unknown") check() end test_crash() test_timeout() test_error_1() test_error_2() test_signal_1() test_signal_2() test_unknown() asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/proclib_wait/000077500000000000000000000000001242304700500250235ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/proclib_wait/test.lua000066400000000000000000000011011242304700500264760ustar00rootroot00000000000000-- test proc:wait print("executing 'sleep 1' then wait()") p = proc.exec("sleep", 1) res, err = proc.perror(p:wait()) fail_if(res ~= 0, "error waiting for sleep, res == " .. tostring(res) .. " (expected 0)") print("executing 'sleep 1', then wait(10)") p = proc.exec("sleep", 1) res, err = proc.perror(p:wait(10)) fail_if(res ~= nil and err ~= "timeout", "expected timeout") print("executing 'sleep 1', then wait(1500)") p = proc.exec("sleep", 1) res, err = proc.perror(p:wait(1500)) fail_if(res ~= 0, "error waiting for sleep, res == " .. tostring(res) .. " (expected 0)") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/spawn_asterisk/000077500000000000000000000000001242304700500254025ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/spawn_asterisk/test.lua000066400000000000000000000007601242304700500270670ustar00rootroot00000000000000-- test spawning asterisk skip_if(not ast.exists(), "asterisk not found") count = 5 instances = {} for i=1,count do print("starting asterisk instance " .. i) local a = ast.new() a:spawn() table.insert(instances, a) end for i=1,count do print("killing asterisk instance " .. i) local a = instances[i] local res, err = proc.perror(a:term_or_kill()) if res == nil then fail("error running asterisk") elseif res ~= 0 then fail("error, asterisk exited with status " .. res) end end asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_atexit1/000077500000000000000000000000001242304700500247635ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_atexit1/test.lua000066400000000000000000000006071242304700500264500ustar00rootroot00000000000000function do_error(result) fail_if(result.result ~= "fail", "expected result to be 'fail' here in do_error()") print("I am going to cause an error") this_will_cause_an_error() end function do_pass(result) fail_if(result.result ~= "fail", "expected result to be 'fail' here in do_pass()") pass() end test.atexit(do_error) test.atexit(do_pass) fail("this test shouldn't actually fail") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_atexit2/000077500000000000000000000000001242304700500247645ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_atexit2/test.lua000066400000000000000000000005641242304700500264530ustar00rootroot00000000000000function do_error(result) fail_if(result.result ~= "pass", "expected 'pass' here in do_error()") print("I am going to cause an error") this_will_cause_an_error() end function do_nothing(result) fail_if(result.result ~= "pass", "expected 'pass' here in do_nothing()") end test.atexit(do_error) test.atexit(do_nothing) test.atexit(do_nothing) test.atexit(do_nothing) asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_atexit3/000077500000000000000000000000001242304700500247655ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_atexit3/test.lua000066400000000000000000000006401242304700500264470ustar00rootroot00000000000000function do_fail(result) fail_if(result.result ~= "pass", "result in do_fail() was not 'pass'") fail() end function do_pass(result) fail_if(result.result ~= "pass", "result in do_pass() was not 'pass'") pass() end function do_nothing(result) fail_if(result.result ~= "pass", "result in do_nothing() was not 'pass'") end test.atexit(do_fail) test.atexit(do_pass) test.atexit(do_pass) test.atexit(do_nothing) asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_error/000077500000000000000000000000001242304700500245355ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_error/test.lua000066400000000000000000000006451242304700500262240ustar00rootroot00000000000000-- spawn the test driver to test errors with various causes require "asttest" a = asttest.run("tests") fail_if(a.results["generated_error"] ~= "error", "generated_error test failed") fail_if(a.results["missing_test_file"] ~= "error", "missing test file test failed") fail_if(a.results["runtime_error"] ~= "error", "runtime error test failed") fail_if(a.results["syntax_error"] ~= "error", "syntax error test failed") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_error/tests/000077500000000000000000000000001242304700500256775ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_error/tests/generated_error/000077500000000000000000000000001242304700500310465ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_error/tests/generated_error/test.lua000066400000000000000000000001231242304700500325240ustar00rootroot00000000000000-- test handling of a user generated error error("this is a user generated error") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_error/tests/missing_test_file/000077500000000000000000000000001242304700500314065ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_error/tests/missing_test_file/readme.txt000066400000000000000000000002151242304700500334020ustar00rootroot00000000000000The test.lua file for this test is intentionally missing to test the behavior of the test driver in the presence of a missing test.lua file. asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_error/tests/runtime_error/000077500000000000000000000000001242304700500305735ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_error/tests/runtime_error/test.lua000066400000000000000000000001631242304700500322550ustar00rootroot00000000000000-- test the handling of runtime errors -- attempt to call a function that does not exist generate_runtime_error() asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_error/tests/syntax_error/000077500000000000000000000000001242304700500304365ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_error/tests/syntax_error/test.lua000066400000000000000000000001201242304700500321110ustar00rootroot00000000000000-- test handling of syntax errors s1 = "string" s2 = "syntax error, missing \" asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_fail/000077500000000000000000000000001242304700500243175ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_fail/test.lua000066400000000000000000000002461242304700500260030ustar00rootroot00000000000000-- spawn the test driver to test failure require "asttest" a = asttest.run("tests") fail_if(a.results["standard_fail"] ~= "fail", "fail test failed, how ironic") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_fail/tests/000077500000000000000000000000001242304700500254615ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_fail/tests/standard_fail/000077500000000000000000000000001242304700500302545ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_fail/tests/standard_fail/test.lua000066400000000000000000000000531242304700500317340ustar00rootroot00000000000000-- test fail fail("this test should fail") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_pass/000077500000000000000000000000001242304700500243525ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_pass/test.lua000066400000000000000000000003341242304700500260340ustar00rootroot00000000000000-- spawn the test driver to test passing require "asttest" a = asttest.run("tests") fail_if(a.results["explicit_pass"] ~= "pass", "pass test failed") fail_if(a.results["implicit_pass"] ~= "pass", "pass test failed") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_pass/tests/000077500000000000000000000000001242304700500255145ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_pass/tests/explicit_pass/000077500000000000000000000000001242304700500303635ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_pass/tests/explicit_pass/test.lua000066400000000000000000000000741242304700500320460ustar00rootroot00000000000000-- test explicitly passing pass("this is an explicit pass") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_pass/tests/implicit_pass/000077500000000000000000000000001242304700500303545ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_pass/tests/implicit_pass/test.lua000066400000000000000000000001001242304700500320250ustar00rootroot00000000000000-- test implicitily passing -- this test will implicitily pass asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_skip/000077500000000000000000000000001242304700500243525ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_skip/test.lua000066400000000000000000000005411242304700500260340ustar00rootroot00000000000000-- spawn the test driver to test skip functionality require "asttest" a = asttest.run("tests") fail_if(a.results["standard_skip"] ~= "skip", "skip test failed") fail_if(a.results["_auto_skip"] ~= "skip", "_auto_skip was not automatically skipped") fail_if(a.results[".hidden_skip"] ~= nil, ".hidden_skip was not automatically and silently skipped") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_skip/tests/000077500000000000000000000000001242304700500255145ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_skip/tests/.hidden_skip/000077500000000000000000000000001242304700500300535ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_skip/tests/.hidden_skip/test.lua000066400000000000000000000001141242304700500315310ustar00rootroot00000000000000-- test skip fail("this test should be silently and automatically skipped") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_skip/tests/_auto_skip/000077500000000000000000000000001242304700500276515ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_skip/tests/_auto_skip/test.lua000066400000000000000000000000771242304700500313370ustar00rootroot00000000000000-- test skip fail("this test should be automatically skipped") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_skip/tests/standard_skip/000077500000000000000000000000001242304700500303425ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_skip/tests/standard_skip/test.lua000066400000000000000000000000441242304700500320220ustar00rootroot00000000000000-- test skip skip("skip this test") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_testlib/000077500000000000000000000000001242304700500250525ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_testlib/test.lua000066400000000000000000000010721242304700500265340ustar00rootroot00000000000000-- test the test library -- note: ts_log() and print() are not tested require "asttest" a = asttest.run("tests") fail_if(a.results["fail_if_true"] ~= "fail", "fail_if(true) did nto cause a failure") fail_if(a.results["fail_if_false"] ~= "pass", "fail_if(false) test failed, it should have passed") fail_if(a.results["fail"] ~= "fail", "fail() did not cause a failure") fail_if(a.results["pass"] ~= "pass", "pass() did not pass") fail_if(a.results["skip"] ~= "skip", "skip() did not skip") fail_if(a.results["error"] ~= "error", "error() did not result in an error") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_testlib/tests/000077500000000000000000000000001242304700500262145ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_testlib/tests/error/000077500000000000000000000000001242304700500273455ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_testlib/tests/error/test.lua000066400000000000000000000000261242304700500310250ustar00rootroot00000000000000-- test error error() asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_testlib/tests/fail/000077500000000000000000000000001242304700500271275ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_testlib/tests/fail/test.lua000066400000000000000000000000241242304700500306050ustar00rootroot00000000000000-- test fail fail() asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_testlib/tests/fail_if_false/000077500000000000000000000000001242304700500307575ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_testlib/tests/fail_if_false/test.lua000066400000000000000000000000371242304700500324410ustar00rootroot00000000000000-- test fail_if fail_if(false) asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_testlib/tests/fail_if_true/000077500000000000000000000000001242304700500306445ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_testlib/tests/fail_if_true/test.lua000066400000000000000000000000361242304700500323250ustar00rootroot00000000000000-- test fail_if fail_if(true) asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_testlib/tests/pass/000077500000000000000000000000001242304700500271625ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_testlib/tests/pass/test.lua000066400000000000000000000000241242304700500306400ustar00rootroot00000000000000-- test pass pass() asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_testlib/tests/skip/000077500000000000000000000000001242304700500271625ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_testlib/tests/skip/test.lua000066400000000000000000000000241242304700500306400ustar00rootroot00000000000000-- test skip skip() asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_xfail/000077500000000000000000000000001242304700500245075ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_xfail/test.lua000066400000000000000000000002531242304700500261710ustar00rootroot00000000000000-- spawn the test driver to test expected failures require "asttest" a = asttest.run("tests") fail_if(a.results["standard_xfail"] ~= "xfail", "standard xfail failed") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_xfail/tests/000077500000000000000000000000001242304700500256515ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_xfail/tests/standard_xfail/000077500000000000000000000000001242304700500306345ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_xfail/tests/standard_xfail/test.lua000066400000000000000000000001001242304700500323050ustar00rootroot00000000000000-- test xfail test.xfail() fail("this is an expected failure") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_xpass/000077500000000000000000000000001242304700500245425ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_xpass/test.lua000066400000000000000000000003671242304700500262320ustar00rootroot00000000000000-- spawn the test driver to test unexpected successes require "asttest" a = asttest.run("tests") fail_if(a.results["implicit_xpass"] ~= "xpass", "implicit xpass failed") fail_if(a.results["explicit_xpass"] ~= "xpass", "explicit xpass failed") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_xpass/tests/000077500000000000000000000000001242304700500257045ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_xpass/tests/explicit_xpass/000077500000000000000000000000001242304700500307435ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_xpass/tests/explicit_xpass/test.lua000066400000000000000000000001331242304700500324220ustar00rootroot00000000000000-- test xpass/xfail with an explicit pass test.xfail() pass("this is an unexpected pass") asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_xpass/tests/implicit_xpass/000077500000000000000000000000001242304700500307345ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/test_xpass/tests/implicit_xpass/test.lua000066400000000000000000000001321242304700500324120ustar00rootroot00000000000000-- test xpass/xfail with an implicit pass test.xfail() -- this test will implicitly pass asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/watcherlib/000077500000000000000000000000001242304700500244715ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/self-tests/watcherlib/test.lua000066400000000000000000000171611242304700500261610ustar00rootroot00000000000000-- test matching various sequences of events require "watcher" dummy_manager = {} dummy_manager.__index = dummy_manager function dummy_manager:new() local dm = { handler = nil, done = false, events = {}, } setmetatable(dm, self) return dm end function dummy_manager:queue(event) table.insert(self.events, event) end function dummy_manager:register_event(event, handler) self.handler = handler end function dummy_manager:unregister_event(event, handler) self.handler = nil end function dummy_manager:pump_messages() return true end function dummy_manager:process_events() if self.done then return end for i, event in ipairs(self.events) do self.handler(event) end self.done = true end function dummy_manager:resend() self.done = false end function standard_dm() local dm = dummy_manager:new() for i=1,10 do e = watcher.event.new("Event" .. i) e["header1"] = "value1" e["header2"] = "value2" e["header3"] = "value3" e["header4"] = "value4" dm:queue(e) end return dm end function empty_dm() return dummy_manager:new() end function expect_timeout(res, err) if res or err ~= "timeout" then fail("watcher did not timeout as expected\nres: " .. tostring(res) .. "\nerr: " .. tostring(err)) end return res, err end function expect_success(res, err) if not res then fail("error running watcher: " .. err) end return res, err end function match_single_tree() -- build a tree with 10 nodes ordered in a single branch ending with -- one leaf, then match it print("matching a single ordered tree of nodes") local tree = watcher.etree:new(watcher.event.new("Event1")) local etree = tree for i=2,10 do etree = etree:add(watcher.event.new("Event" .. i)) end expect_success(watcher.watch(standard_dm(), tree, 100)) end function match_multiple_trees() -- build a tree two trees then match one of them print("matching multiple trees of ordered tree of nodes") function build_tree(start, finish) local tree = watcher.etree:new(watcher.event.new("Event" .. start)) local etree = tree for i=start+1,finish do etree = etree:add(watcher.event.new("Event" .. i)) end end expect_success(watcher.watch(standard_dm(), {build_tree(5, 10), build_tree(1, 3)}, 100)) end function match_branching_tree_1() -- build a tree with a few nodes in a branching configuration print("matching an ordered tree with branching nodes") -- build a tree with a few nodes in a branching configuration mixed in -- with some out of order nodes print("matching events with headers (1)") local e = watcher.event.new -- build the follwing tree -- Event1 -- |-> {Event4, Event2} -- |-> Event24 -- |-> Event5 -> {Event7, Event8} -> Event10 -- |-> Event3 -> Event12 -- |-> Event23 local tree = watcher.etree:new(watcher.event.new("Event1")) local t2 = tree:add{e("Event4"), e("Event2")} t2:add(e("Event24")) t2:add(e("Event5")) :add{e("Event7"), e("Event8")} :add(e("Event10")) tree:add(e("Event3")) :add(e("Event12")) tree:add(e("Event23")) expect_success(watcher.watch(standard_dm(), tree, 100)) local tree = watcher.etree:new(watcher.event.new("Event1")) local etree = tree for i=2,10 do etree = etree:add(watcher.event.new("Event" .. i)) end expect_success(watcher.watch(standard_dm(), tree, 100)) end function match_out_of_order() -- match 9 events out of order followed by a 10th event print("testing matching out of order") -- build the events table with Event9 through Event1 in that order local events = {} for i=9,1,-1 do table.insert(events, watcher.event.new("Event" .. i)) end local tree = watcher.etree:new(events) tree:add(watcher.event.new("Event10")) expect_success(watcher.watch(standard_dm(), tree, 100)) -- check and make sure every event was actually matched for i=1,9 do if not tree.received[i] then fail("tree.multi[" .. i .. "] is: " .. tostring(tree.multi[i])) end end end function match_branching_out_of_order_tree_1() -- build a tree with a few nodes in a branching configuration mixed in -- with some out of order nodes print("matching an ordered tree with branching nodes and out of order nodes") local e = watcher.event.new -- build the follwing tree -- Event1 -- |-> {Event4, Event2} -> Event5 -> {Event7, Event8} -> Event10 -- |-> Event3 -> Event12 -- |-> Event23 local tree = watcher.etree:new(watcher.event.new("Event1")) tree:add{e("Event4"), e("Event2")} :add(e("Event5")) :add{e("Event7"), e("Event8")} :add(e("Event10")) tree:add(e("Event3")) :add(e("Event12")) tree:add(e("Event23")) expect_success(watcher.watch(standard_dm(), tree, 100)) end function match_headers_1() -- build a tree with a few nodes in a branching configuration mixed in -- with some out of order nodes print("matching events with headers (1)") local e = function(event) local e = watcher.event.new(event) e["header1"] = "value1" e["header2"] = "value2" e["header3"] = "value3" e["header4"] = "value4" return e end -- build the follwing tree -- Event1 -- |-> {Event4, Event2} -> Event5 -> {Event7, Event8} -> Event10 -- |-> Event3 -> Event12 -- |-> Event23 local tree = watcher.etree:new(watcher.event.new("Event1")) tree:add{e("Event4"), e("Event2")} :add(e("Event5")) :add{e("Event7"), e("Event8")} :add(e("Event10")) tree:add(e("Event3")) :add(e("Event12")) tree:add(e("Event23")) expect_success(watcher.watch(standard_dm(), tree, 100)) end function match_headers_2() -- build a tree with a few nodes in a branching configuration mixed in -- with some out of order nodes print("matching events with headers (2)") local e = function(event) local e = watcher.event.new(event) e["header1"] = "value1" e["header2"] = "value2" e["header3"] = "value3" e["header4"] = "won't match" return e end -- build the follwing tree -- Event1 -- |-> {Event4, Event2} -> Event5 -> {Event7, Event8} -> Event10 -- |-> Event3 -> Event12 -- |-> Event24 local tree = watcher.etree:new(watcher.event.new("Event1")) tree:add{e("Event4"), e("Event2")} :add(e("Event5")) :add{e("Event7"), e("Event8")} :add(e("Event10")) tree:add(e("Event3")) :add(e("Event12")) tree:add(e("Event23")) expect_timeout(watcher.watch(standard_dm(), tree, 100)) end function test_timeout_1() -- build a tree starting with 'Event10' which will come last causing a -- timeout print("testing a timeout (1)") local tree = watcher.etree:new(watcher.event.new("Event10")) local etree = tree for i=1,9 do etree = etree:add(watcher.event.new("Event" .. i)) end expect_timeout(watcher.watch(standard_dm(), tree, 100)) end function test_timeout_2() -- don't send any events causing a timeout print("testing a timeout (2)") local tree = watcher.etree:new(watcher.event.new("Event10")) local etree = tree for i=1,9 do etree = etree:add(watcher.event.new("Event" .. i)) end expect_timeout(watcher.watch(empty_dm(), tree, 100)) end function match_with_function_1() print("testing matching with a matching function (1)") local tree = watcher.etree:new(function(e) return true end) expect_success(watcher.watch(standard_dm(), tree, 100)) end function match_with_function_2() print("testing matching headers with a matching function (1)") local event = watcher.event.new("Event3") event["header1"] = function(value) return value == "value1" end local tree = watcher.etree:new(event) expect_success(watcher.watch(standard_dm(), tree, 100)) end match_single_tree() match_multiple_trees() match_branching_tree_1() match_out_of_order() match_branching_out_of_order_tree_1() match_headers_1() match_headers_2() test_timeout_1() test_timeout_2() match_with_function_1() match_with_function_2() asterisk-testsuite-0.0.0+svn.5781/asttest/tools/000077500000000000000000000000001242304700500214145ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/asttest/tools/lfs.diff000066400000000000000000000017171242304700500230400ustar00rootroot00000000000000diff -ruN a/config b/config --- a/config 2007-10-27 17:42:32.000000000 -0500 +++ b/config 2010-06-22 15:27:10.000000000 -0500 @@ -18,7 +18,8 @@ # Compilation directives WARN= -O2 -Wall -fPIC -W -Waggregate-return -Wcast-align -Wmissing-prototypes -Wnested-externs -Wshadow -Wwrite-strings -pedantic INCS= -I$(LUA_INC) -CFLAGS= $(WARN) $(INCS) +LUA_CFLAGS=`pkg-config --cflags lua5.1 2> /dev/null || pkg-config --cflags lua 2> /dev/null` +CFLAGS= $(WARN) $(LUA_CFLAGS) CC= gcc # $Id: config,v 1.21 2007/10/27 22:42:32 carregal Exp $ diff -ruN a/src/lfs.c b/src/lfs.c --- a/src/lfs.c 2009-02-03 16:05:48.000000000 -0600 +++ b/src/lfs.c 2010-06-22 15:27:15.000000000 -0500 @@ -232,7 +232,8 @@ } } #else -static int lfs_g_setmode (lua_State *L, FILE *f, int arg) { +#define lfs_g_setmode(L, f, arg) __lfs_g_setmode(L) +static int __lfs_g_setmode (lua_State *L) { lua_pushboolean(L, 0); lua_pushliteral(L, "setmode not supported on this platform"); return 2; asterisk-testsuite-0.0.0+svn.5781/asttest/tools/luaposix.diff000066400000000000000000000041321242304700500241120ustar00rootroot00000000000000diff -ruN a/lposix.c b/lposix.c --- a/lposix.c 2008-07-18 09:30:36.000000000 -0500 +++ b/lposix.c 2010-06-24 14:49:07.000000000 -0500 @@ -9,6 +9,7 @@ */ #include +#include #include #include #include @@ -575,6 +576,12 @@ return 1; } +static int Pusleep(lua_State *L) /** usleep(usec) */ +{ + useconds_t usec = luaL_checklong(L, 1); + return pushresult(L, usleep(usec), NULL); +} + static int Psetenv(lua_State *L) /** setenv(name,value,[over]) */ { @@ -701,6 +708,24 @@ return doselection(L, 1, SgetID, FgetID, NULL); } +static int Pgettimeofday(lua_State *L) /** gettimeofday() */ +{ + /* NOTE: the tz argument is not supported as the man page says it is + * obsolete */ + struct timeval tv; + + if (gettimeofday(&tv, NULL)) { + return pusherror(L, NULL); + } + + lua_createtable(L, 0, 2); + lua_pushnumber(L, tv.tv_sec); + lua_setfield(L, -2, "tv_sec"); + + lua_pushnumber(L, tv.tv_usec); + lua_setfield(L, -2, "tv_usec"); + return 1; +} static int Phostid(lua_State *L) /** hostid() */ { @@ -1055,6 +1080,7 @@ {"getlogin", Pgetlogin}, {"getpasswd", Pgetpasswd}, {"getpid", Pgetpid}, + {"gettimeofday", Pgettimeofday}, {"glob", Pglob}, {"hostid", Phostid}, {"kill", Pkill}, @@ -1069,6 +1095,7 @@ {"setenv", Psetenv}, {"setpid", Psetpid}, {"sleep", Psleep}, + {"usleep", Pusleep}, {"stat", Pstat}, {"sysconf", Psysconf}, {"times", Ptimes}, Binary files a/.lposix.c.swp and b/.lposix.c.swp differ diff -ruN a/lposix.h b/lposix.h --- a/lposix.h 1969-12-31 18:00:00.000000000 -0600 +++ b/lposix.h 2010-06-22 15:29:02.000000000 -0500 @@ -0,0 +1,6 @@ +#ifndef LPOSIX_H +#define LPOSIX_H + +LUALIB_API int luaopen_posix (lua_State *L); + +#endif diff -ruN a/Makefile b/Makefile --- a/Makefile 2008-07-18 09:31:56.000000000 -0500 +++ b/Makefile 2010-06-22 15:29:02.000000000 -0500 @@ -25,7 +25,7 @@ CPPFLAGS= -fPIC $(INCS) $(WARN) WARN= -pedantic -Wall -INCS= -I$(LUAINC) +INCS= `pkg-config --cflags lua5.1 2> /dev/null || pkg-config --cflags lua 2> /dev/null` MYNAME= posix MYLIB= $(MYNAME) asterisk-testsuite-0.0.0+svn.5781/asttest/tools/luasocket.diff000066400000000000000000000027601242304700500242450ustar00rootroot00000000000000diff -ruN a/config b/config --- a/config 2007-10-14 23:21:05.000000000 -0500 +++ b/config 2010-06-22 15:02:13.000000000 -0500 @@ -11,13 +11,12 @@ SOCKET_SO=socket.$(EXT).$(SOCKET_V) MIME_SO=mime.$(EXT).$(MIME_V) UNIX_SO=unix.$(EXT) +SOCKET_A=luasocket.a #------ # Lua includes and libraries # -#LUAINC=-I/usr/local/include/lua50 -#LUAINC=-I/usr/local/include/lua5.1 -#LUAINC=-Ilua-5.1.1/src +LUAINC=`pkg-config --cflags lua5.1 2> /dev/null || pkg-config --cflags lua 2> /dev/null` #------ # Compat-5.1 directory @@ -54,6 +53,10 @@ CFLAGS= $(LUAINC) $(DEF) -pedantic -Wall -O2 -fpic LDFLAGS=-O -shared -fpic LD=gcc +AR=ar +ARFLAGS=rc +RANLIB=ranlib +RANLIBFLAGS= #------ # End of makefile configuration diff -ruN a/src/makefile b/src/makefile --- a/src/makefile 2007-10-14 23:21:05.000000000 -0500 +++ b/src/makefile 2010-06-10 15:29:05.000000000 -0500 @@ -47,7 +47,7 @@ usocket.o \ unix.o -all: $(SOCKET_SO) $(MIME_SO) +all: $(SOCKET_SO) $(MIME_SO) $(SOCKET_A) $(SOCKET_SO): $(SOCKET_OBJS) $(LD) $(LDFLAGS) -o $@ $(SOCKET_OBJS) @@ -58,6 +58,11 @@ $(UNIX_SO): $(UNIX_OBJS) $(LD) $(LDFLAGS) -o $@ $(UNIX_OBJS) +$(SOCKET_A): $(SOCKET_OBJS) + $(AR) $(ARFLAGS) $@ $(SOCKET_OBJS) + $(RANLIB) $(RANLIBFLAGS) $@ + + #------ # List of dependencies # @@ -82,7 +87,7 @@ usocket.o: usocket.c socket.h io.h timeout.h usocket.h clean: - rm -f $(SOCKET_SO) $(SOCKET_OBJS) + rm -f $(SOCKET_SO) $(SOCKET_A) $(SOCKET_OBJS) rm -f $(MIME_SO) $(UNIX_SO) $(MIME_OBJS) $(UNIX_OBJS) #------ asterisk-testsuite-0.0.0+svn.5781/asttest/tools/mkstring.c000066400000000000000000000045341242304700500234240ustar00rootroot00000000000000/* * Asterisk -- An open source telephony toolkit. * * Copyright (C) 1999 - 2008, Digium, Inc. * * Matthew Nicholson * * See http://www.asterisk.org for more information about * the Asterisk project. Please do not directly contact * any of the maintainers of this project for assistance; * the project provides a web site, mailing lists and IRC * channels for your use. * * This program is free software, distributed under the terms of * the GNU General Public License Version 2. See the LICENSE file * at the top of the source tree. */ #include #include #include #include struct mkstring_options { char *infile; char *outfile; char *name; }; int mkstring(struct mkstring_options *opts) { FILE *infile; FILE *outfile; char c; int count; if (!(infile = fopen(opts->infile, "r"))) { fprintf(stderr, "Error opening file '%s' for reading: %s\n", opts->infile, strerror(errno)); return 1; } if (!(outfile = fopen(opts->outfile, "w"))) { fprintf(stderr, "Error opening file '%s' for wriring: %s\n", opts->outfile, strerror(errno)); fclose(infile); return 1; } fprintf(outfile, "/* This file is automatically generated. DO NOT EDIT. */\n" "\n" "const char %s[] = {\n", opts->name); count = 0; while (fread(&c, sizeof(char), 1, infile)) { count += fprintf(outfile, "%3i, ", c); if (count > 72) { count = 0; fprintf(outfile, "\n"); } } fprintf(outfile, "\n};\n"); fclose(infile); fclose(outfile); return 0; } int parse_cmdline(int argc, char *argv[], struct mkstring_options *opts) { char c; int i; while ((c = getopt(argc, argv, "n:o:")) != -1) { switch (c) { case 'n': opts->name = optarg; break; case 'o': opts->outfile = optarg; break; case '?': return 1; break; } } for (i = optind; i < argc; i++) { opts->infile = argv[i]; } if (!opts->infile || !opts->outfile || !opts->name) { fprintf(stderr, "Missing arguments\n"); return 1; } return 0; } void usage(const char *prog_name) { fprintf(stderr, "Usage:\n"); fprintf(stderr, " %s -n -o \n", prog_name); } int main(int argc, char *argv[]) { struct mkstring_options opts; memset(&opts, 0, sizeof(struct mkstring_options)); if (parse_cmdline(argc, argv, &opts)) { usage(argv[0]); return 1; } return mkstring(&opts); } asterisk-testsuite-0.0.0+svn.5781/cleanup-test-remnants.sh000077500000000000000000000001521242304700500233530ustar00rootroot00000000000000#!/bin/bash find . -type d -name tmp | grep -vF .svn | xargs -d\\n rm -rf rm -rf /tmp/asterisk-testsuite asterisk-testsuite-0.0.0+svn.5781/configs/000077500000000000000000000000001242304700500202155ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/configs/ari.conf000066400000000000000000000001231242304700500216330ustar00rootroot00000000000000[general] enabled = yes pretty = yes [testsuite] type = user password = testsuite asterisk-testsuite-0.0.0+svn.5781/configs/asterisk.options.conf.inc000066400000000000000000000001631242304700500251530ustar00rootroot00000000000000; Global asterisk.conf [options] settings. ; debug = 4 verbose = 4 nocolor = yes dumpcore = yes execincludes = yes asterisk-testsuite-0.0.0+svn.5781/configs/branch-1.4/000077500000000000000000000000001242304700500217525ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/configs/branch-1.4/logger.conf000066400000000000000000000005401242304700500240770ustar00rootroot00000000000000; Global logger.conf settings. ; ; IMPORTANT: Place your variables before #include statements ; so tests have the ability to overwrite them. ; [general] #include "logger.general.conf.inc" [logfiles] console => verbose messages.txt => notice,warning,error,verbose full.txt => notice,warning,error,debug,verbose,dtmf #include "logger.logfiles.conf.inc" asterisk-testsuite-0.0.0+svn.5781/configs/branch-1.4/manager.conf000066400000000000000000000004731242304700500242370ustar00rootroot00000000000000[general] #include "manager.general.conf.inc" enabled = yes webenabled = yes port = 5038 bindaddr = 127.0.0.1 [user] secret = mysecret read = system,call,log,verbose,agent,user,config,dtmf,reporting,cdr,dialplan write = system,call,agent,user,config,command,reporting,originate #include "manager.users.conf.inc" asterisk-testsuite-0.0.0+svn.5781/configs/branch-1.6.2/000077500000000000000000000000001242304700500221145ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/configs/branch-1.6.2/logger.conf000066400000000000000000000005401242304700500242410ustar00rootroot00000000000000; Global logger.conf settings. ; ; IMPORTANT: Place your variables before #include statements ; so tests have the ability to overwrite them. ; [general] #include "logger.general.conf.inc" [logfiles] console => verbose messages.txt => notice,warning,error,verbose full.txt => notice,warning,error,debug,verbose,dtmf #include "logger.logfiles.conf.inc" asterisk-testsuite-0.0.0+svn.5781/configs/bridge/000077500000000000000000000000001242304700500214515ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast1/000077500000000000000000000000001242304700500223215ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast1/features.conf000066400000000000000000000002021242304700500250000ustar00rootroot00000000000000[general] atxferthreeway => 7 [featuremap] blindxfer => 1 atxfer => 2 disconnect => 3 automon => 4 automixmon => 5 parkcall => 6 asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast1/iax.conf000066400000000000000000000002051242304700500237460ustar00rootroot00000000000000[general] bindaddr=127.0.0.1 bindport=4569 [alice] type=friend host=127.0.0.1 port=4570 [bob] type=friend host=127.0.0.1 port=4571 asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast1/sip-alice.conf000066400000000000000000000000211242304700500250270ustar00rootroot00000000000000[alice-extra](!) asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast1/sip-bob.conf000066400000000000000000000000171242304700500245210ustar00rootroot00000000000000[bob-extra](!) asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast1/sip.conf000066400000000000000000000004241242304700500237630ustar00rootroot00000000000000[general] udpbindaddr = 127.0.0.1:5060 #include sip-alice.conf #include sip-bob.conf [alice](alice-extra) sendrpid=pai callerid=Alice <1234> type = peer host = 127.0.0.1 port = 5061 [bob](bob-extra) sendrpid=pai callerid=Bob <4321> type = peer host = 127.0.0.1 port = 5062 asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast2/000077500000000000000000000000001242304700500223225ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast2/extensions-extra.conf000066400000000000000000000003401242304700500265060ustar00rootroot00000000000000; This file is intentionally blank. If a specific bridge test needs to add ; extra global variables, extensions, or contexts for a specific test, then ; an extensions-extra.conf file can be added into test_dir/configs/ast2/ asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast2/extensions.conf000066400000000000000000000011521242304700500253670ustar00rootroot00000000000000;Alice's dialplan #include extensions-extra.conf [default] ; Originated call is linked to this extension exten => test_call,1,Playback(${TALK_AUDIO}) same => n,SendDTMF(#) same => n,Record(alice_audio_%d.wav,,,k) same => n,UserEvent(Connected, Channel: ${CHANNEL(name)}) same => n,BackgroundDetect(${RECORDED_FILE},1,20,,20000) same => n,GoToIf($[${TALK_DETECTED}=0]?talkdetectfail:talkdetectpass) same => n(talkdetectfail),NoOp() same => n,UserEvent(TalkDetect, result: fail) same => n,Hangup() same => n(talkdetectpass),NoOp() same => n,UserEvent(TalkDetect, result: pass) same => n,Wait(10000) same => n,Hangup() asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast2/iax.conf000066400000000000000000000001261242304700500237510ustar00rootroot00000000000000[general] binaddr=127.0.0.1 bindport=4570 [uut] type=friend host=127.0.0.1 port=4569 asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast2/sip-uut.conf000066400000000000000000000000171242304700500245750ustar00rootroot00000000000000[uut-extra](!) asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast2/sip.conf000066400000000000000000000001611242304700500237620ustar00rootroot00000000000000[general] udpbindaddr=127.0.0.1:5061 #include sip-uut.conf [uut](uut-extra) type=peer host=127.0.0.1 port=5060 asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast3/000077500000000000000000000000001242304700500223235ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast3/extensions-extra.conf000066400000000000000000000003401242304700500265070ustar00rootroot00000000000000; This file is intentionally blank. If a specific bridge test needs to add ; extra global variables, extensions, or contexts for a specific test, then ; an extensions-extra.conf file can be added into test_dir/configs/ast3/ asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast3/extensions.conf000066400000000000000000000005501242304700500253710ustar00rootroot00000000000000;Bob's dialplan #include extensions-extra.conf ; Incoming call comes here [default] exten => test_call,1,Record(bob_audio_%d.wav,,,k) same => n,Playback(${RECORDED_FILE}) same => n,SendDTMF(#) same => n,UserEvent(Connected, Channel: ${CHANNEL(name)}) same => n,Wait(10000) same => n,Hangup() same => n(fail),UserEvent(DTMF, Success: False) same => n,Hangup() asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast3/iax.conf000066400000000000000000000001271242304700500237530ustar00rootroot00000000000000[general] bindaddr=127.0.0.1 bindport=4571 [uut] type=friend host=127.0.0.1 port=4569 asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast3/sip-uut.conf000066400000000000000000000000171242304700500245760ustar00rootroot00000000000000[uut-extra](!) asterisk-testsuite-0.0.0+svn.5781/configs/bridge/ast3/sip.conf000066400000000000000000000001611242304700500237630ustar00rootroot00000000000000[general] udpbindaddr=127.0.0.1:5062 #include sip-uut.conf [uut](uut-extra) type=peer host=127.0.0.1 port=5060 asterisk-testsuite-0.0.0+svn.5781/configs/cdr.conf000066400000000000000000000001351242304700500216330ustar00rootroot00000000000000[general] unanswered=no [csv] usegmtime=yes loguniqueid=yes loguserfield=yes accountlogs=yesasterisk-testsuite-0.0.0+svn.5781/configs/cel.conf000066400000000000000000000001141242304700500216230ustar00rootroot00000000000000[general] enable=yes apps=dial,park,queue events=ALL [manager] enabled=yes asterisk-testsuite-0.0.0+svn.5781/configs/cel_custom.conf000066400000000000000000000012371242304700500232240ustar00rootroot00000000000000[mappings] Master.csv => ${CSV_QUOTE(${eventtype})},${CSV_QUOTE(${eventtime})},${CSV_QUOTE(${CALLERID(name)})},${CSV_QUOTE(${CALLERID(num)})},${CSV_QUOTE(${CALLERID(ANI)})},${CSV_QUOTE(${CALLERID(RDNIS)})},${CSV_QUOTE(${CALLERID(DNID)})},${CSV_QUOTE(${CHANNEL(exten)})},${CSV_QUOTE(${CHANNEL(context)})},${CSV_QUOTE(${CHANNEL(channame)})},${CSV_QUOTE(${CHANNEL(appname)})},${CSV_QUOTE(${CHANNEL(appdata)})},${CSV_QUOTE(${CHANNEL(amaflags)})},${CSV_QUOTE(${CHANNEL(accountcode)})},${CSV_QUOTE(${CHANNEL(uniqueid)})},${CSV_QUOTE(${CHANNEL(linkedid)})},${CSV_QUOTE(${BRIDGEPEER})},${CSV_QUOTE(${CHANNEL(userfield)})},${CSV_QUOTE(${userdeftype})},${CSV_QUOTE(${eventextra})} asterisk-testsuite-0.0.0+svn.5781/configs/cli_aliases.conf000066400000000000000000000001201242304700500233250ustar00rootroot00000000000000[general] template = testsuite [testsuite] core set global=dialplan set global asterisk-testsuite-0.0.0+svn.5781/configs/conflicts.txt000066400000000000000000000012671242304700500227500ustar00rootroot00000000000000;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; this file contains a key/value mapping of conflicts between loadable modules. ;; ;; if a test has a dependency (the key) it will not load the associated comma ;; separated list of modules (the value). ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; chan_sip and chan_pjsip don't play nice with each other unless configured ;; correctly so don't load chan_sip if it is a chan/res_pjsip test. res_pjsip=chan_sip.so ;; and don't load chan_pjsip if the test uses chan_sip chan_sip=chan_pjsip.so,res_pjsip.so,res_pjsip_pubsub.so,res_pjsip_session.so,res_pjsip_outbound_publish.so asterisk-testsuite-0.0.0+svn.5781/configs/http.conf000066400000000000000000000000771242304700500220470ustar00rootroot00000000000000[general] enabled=yes bindaddr=127.0.0.1 bindport=8088 prefix= asterisk-testsuite-0.0.0+svn.5781/configs/logger.conf000066400000000000000000000004771242304700500223530ustar00rootroot00000000000000; Global logger.conf settings. ; ; IMPORTANT: Place your variables before #include statements ; so tests have the ability to overwrite them. ; [general] #include "logger.general.conf.inc" [logfiles] console => verbose messages.txt => notice,warning,error,dtmf,verbose full.txt => * #include "logger.logfiles.conf.inc" asterisk-testsuite-0.0.0+svn.5781/configs/logger.general.conf.inc000066400000000000000000000000201242304700500245170ustar00rootroot00000000000000; Do not delete asterisk-testsuite-0.0.0+svn.5781/configs/logger.logfiles.conf.inc000066400000000000000000000000201242304700500247060ustar00rootroot00000000000000; Do not delete asterisk-testsuite-0.0.0+svn.5781/configs/manager.conf000066400000000000000000000005201242304700500224730ustar00rootroot00000000000000[general] enabled = yes webenabled = yes port = 5038 bindaddr = 127.0.0.1 #include "manager.general.conf.inc" [user] secret = mysecret read = system,call,log,verbose,agent,user,config,dtmf,reporting,cdr,dialplan,test,agi write = system,call,agent,user,config,command,reporting,originate,agi,message #include "manager.users.conf.inc" asterisk-testsuite-0.0.0+svn.5781/configs/manager.general.conf.inc000066400000000000000000000000201242304700500246520ustar00rootroot00000000000000; Do not delete asterisk-testsuite-0.0.0+svn.5781/configs/manager.users.conf.inc000066400000000000000000000000201242304700500243760ustar00rootroot00000000000000; Do not delete asterisk-testsuite-0.0.0+svn.5781/configs/modules.conf000066400000000000000000000002671242304700500225410ustar00rootroot00000000000000; Do not delete - ; May be appended with noloads due to conflicts (see conflicts.txt) [modules] autoload=yes noload => chan_alsa.so noload => chan_oss.so noload => chan_console.so asterisk-testsuite-0.0.0+svn.5781/configs/res_config_sqlite.conf000066400000000000000000000000121242304700500245540ustar00rootroot00000000000000[general] asterisk-testsuite-0.0.0+svn.5781/configs/res_pgsql.conf000066400000000000000000000000121242304700500230540ustar00rootroot00000000000000[general] asterisk-testsuite-0.0.0+svn.5781/configs/res_pktccops.conf000066400000000000000000000000121242304700500235540ustar00rootroot00000000000000[general] asterisk-testsuite-0.0.0+svn.5781/contrib/000077500000000000000000000000001242304700500202255ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/contrib/scripts/000077500000000000000000000000001242304700500217145ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/contrib/scripts/cel-to-yaml.py000077500000000000000000000033271242304700500244210ustar00rootroot00000000000000#!/usr/bin/env python """Script that turns a CEL CSV file into a blob of YAML suitable for parsing by the CELModule Copyright (C) 2013, Digium, Inc. Matt Jordan This program is free software, distributed under the terms of the GNU General Public License Version 2. """ import sys import os import optparse import yaml def main(argv=None): if argv is None: args = sys.argv parser = optparse.OptionParser() parser.add_option("-i", "--input", dest="input_file", help="Specify the input CEL file to process") parser.add_option("-c", "--column", action="append", dest="columns", help="Add a column to the output. Can be used multiple times.") (options, args) = parser.parse_args(argv) columns = ["eventtype", "cidname", "cidnum", "dnid", "exten", "context", "channel", "app"] if options.columns: for c in options.columns: columns.append(c) fields = ['eventtype', 'eventtime', 'cidname', 'cidnum', 'ani', 'rdnis', 'dnid', 'exten', 'context', 'channel', 'app', 'appdata', 'amaflags', 'accountcode', 'uniqueid', 'linkedid', 'bridgepeer', 'userfield', 'userdeftype', 'eventextra'] input_file = open(options.input_file) result = [] for line in input_file.readlines(): tokens = line.split(',') blob = {} for column in columns: index = fields.index(column) blob[column] = str(tokens[index].replace('"','').strip()) result.append(blob) raw_yaml = yaml.dump(result) raw_yaml = raw_yaml.replace(',', '\n').replace('}', '').replace('{', '') print raw_yaml return 0 if __name__ == "__main__": sys.exit(main() or 0)asterisk-testsuite-0.0.0+svn.5781/contrib/scripts/pretty_print000077500000000000000000000030701242304700500244050ustar00rootroot00000000000000#!/bin/bash # # Copyright (C) 2014, Fairview 5 Engineering, LLC # George Joseph # # This program is free software, distributed under the terms of # the GNU General Public License Version 2. # if [ -t 0 ] ; then echo "pretty_print is a filter and needs the output of runtests.py piped to it." echo "Try python -u ./runtests.py | ./pretty_print" echo "The 'python -u' is needed to use unbuffered output mode otherwise you'll only see output in big chunks." exit 1 fi declare -ix passed=0 declare -ix failed=0 declare -ix tests=0 declare -a failures GREEN='\033[01;32m' RED='\033[01;31m' NORM='\033[m' col=$(( $(tput cols) - 28 )) trap break INT while read line ; do if [[ $line =~ ^Running\ tests\ for.* ]] ; then echo $line printf "%-*.*s %4s/${GREEN}%4s${NORM}/${RED}%4s${NORM} ${COLOR} [ %s ]${NORM}\n" $col $col "Test" "Run" "Pass" "Fail" "Status" fi if [[ $line =~ ^Test.*(tests[^\']+)\',.*(passed|failed)$ ]] ; then test=${BASH_REMATCH[1]} status=${BASH_REMATCH[2]} col=$(( $(tput cols) - 28 )) (( tests++ )) if [[ $status = passed ]] ; then (( passed++ )) COLOR=${GREEN} label=Passed fi if [[ $status = failed ]] ; then (( failed++ )) COLOR=${RED} label=Failed failures+=("FAILED: $test") fi printf "%-*.*s %4d/${GREEN}%4d${NORM}/${RED}%4d${NORM} ${COLOR} [ %s ]${NORM}\n" $col $col $test $tests $passed $failed $label fi done trap - INT echo -e "\tTests: $tests\t\t${GREEN}Passed: $passed\t\t${RED}Failed: $failed${NORM}" for fail in "${failures[@]}" ; do echo -e "${RED}$fail${NORM}" done asterisk-testsuite-0.0.0+svn.5781/contrib/scripts/rlmi.xml000066400000000000000000000017501242304700500234040ustar00rootroot00000000000000 Buddy List Liste d'amis Bob Smith Dave Jones Jim Ed asterisk-testsuite-0.0.0+svn.5781/contrib/scripts/rlmi_demo.py000077500000000000000000000061361242304700500242460ustar00rootroot00000000000000#!/usr/bin/env python """Demonstration of parsing and printing RLMI XML document. This script is meant to demonstrate how, given an RLMI XML document, one can use the pyxb-generated rlmi module to parse the XML document into easy-to peruse objects. The XML document used in this demo is from RFC 4662, section 5.1. This script must be run from the base testsuite directory since paths are assumed to originate from there. """ import sys sys.path.append("lib/python") # rlmi is a pyxb-generated module create from the XML schema for RLMI. It # defines classes that are used when an RLMI XML document is parsed. import rlmi xml = open('contrib/scripts/rlmi.xml').read() # CreateFromDocument takes XML text and creates an XML document object that can # be used in ways that Python objects are typically used. list_elem = rlmi.CreateFromDocument(xml) # The outermost XML element in an RLMI document is the 'list' element. print 'list' # pyxb performs type-checking and type conversion of elements that it comes # across in XML documents. In this case, the list element's version attribute is # an integer, and the fullState attribute is a boolean. This is why the str() # function is necessary in order to convert to string for the concatenation. print '\t' + str(list_elem.version) print '\t' + str(list_elem.fullState) print '\t' + list_elem.uri # A list element may have zero or more name elements in it. This is a # user-visible name for the list. The main reason why more than one name may # exist for a list would be because it is expressed in multiple languages. # Asterisk RLMI lists will have only a single name. for name in list_elem.name: print '\tname' # Parsed XML documents have their attributes accessed as members on an # object. print '\t\t' + name.lang # The content of XML elements is accessed through the value() member. print '\t\t' + name.value() # A list element may have zero or more resource elements in it. The resources # represent the resources that make up the list (duh). for resource in list_elem.resource: print '\tresource' print '\t\t' + resource.uri # Resources may have names associated with them, just like the list does. # Asterisk will use the name of the list from the configuration file here. for name in resource.name: print '\t\tname' print '\t\t\t' + name.value() # Resources may have zero or more instance elements on them. The reason that # more than one instance element may exist for a resource is that a resource # may correspond to a single subscription that forks, resulting in multiple # instances of the resource being represented. In Asterisk's case, there # will be a single instance per resource. for instance in resource.instance: print '\t\tinstance' print '\t\t\t' + instance.id print '\t\t\t' + instance.state # If an instance has a cid, it indicates that there is a body element # in the multipart body that corresponds to the instance. The cid # corresponds to the Content-ID header in that body part. if instance.cid: print '\t\t\t' + instance.cid asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/000077500000000000000000000000001242304700500212005ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/calls/000077500000000000000000000000001242304700500222765ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/calls/uac-blind-transfer.xml000066400000000000000000000076001242304700500265030ustar00rootroot00000000000000 ;tag=[pid]SIPpTag00[call_number] To: sut Call-ID: [call_id] CSeq: 1 INVITE Contact: sip:sipp@[local_ip]:[local_port] Max-Forwards: 70 Subject: Performance Test Content-Type: application/sdp Content-Length: [len] v=0 o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip] s=- c=IN IP[media_ip_type] [media_ip] t=0 0 m=audio [media_port] RTP/AVP 0 a=rtpmap:0 PCMU/8000 ]]> ;tag=[pid]SIPpTag00[call_number] To: sut [peer_tag_param] Call-ID: [call_id] CSeq: 1 ACK Contact: sip:sipp@[local_ip]:[local_port] Max-Forwards: 70 Subject: Performance Test Content-Length: 0 ]]> ;tag=[pid]SIPpTag00[call_number] To: sut [peer_tag_param] Call-ID: [call_id] CSeq: 2 REFER Contact: sip:sipp@[local_ip]:[local_port] Max-Forwards: 70 Subject: Performance Test Refer-To: sip:[transfer_target]@[remote_ip]:[remote_port];user=phone Referred-By: sip:sipp@[local_ip]:[local_port] Diversion: ;reason="send_to_vm" Content-Length: 0 ]]> Content-Length: 0 ]]> Content-Length: 0 ]]> ;tag=[pid]SIPpTag00[call_number] To: sut [peer_tag_param] Call-ID: [call_id] CSeq: [cseq] BYE Contact: sip:sipp@[local_ip]:[local_port] Max-Forwards: 70 Subject: Performance Test Content-Length: 0 ]]> asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/calls/uac-hangup.xml000066400000000000000000000044711242304700500250560ustar00rootroot00000000000000 ;tag=[pid]SIPpTag00[call_number] To: sut Call-ID: [call_id] CSeq: 1 INVITE Contact: sip:sipp@[local_ip]:[local_port] Max-Forwards: 70 Subject: Performance Test Content-Type: application/sdp Content-Length: [len] v=0 o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip] s=- c=IN IP[media_ip_type] [media_ip] t=0 0 m=audio [media_port] RTP/AVP 0 a=rtpmap:0 PCMU/8000 ]]> ;tag=[pid]SIPpTag00[call_number] To: sut [peer_tag_param] Call-ID: [call_id] CSeq: 1 ACK Contact: sip:sipp@[local_ip]:[local_port] Max-Forwards: 70 Subject: Performance Test Content-Length: 0 ]]> ;tag=[pid]SIPpTag00[call_number] To: sut [peer_tag_param] Call-ID: [call_id] CSeq: 2 BYE Contact: sip:sipp@[local_ip]:[local_port] Max-Forwards: 70 Subject: Performance Test Content-Length: 0 ]]> asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/calls/uac-no-hangup.xml000066400000000000000000000040751242304700500254700ustar00rootroot00000000000000 ;tag=[pid]SIPpTag00[call_number] To: sut Call-ID: [call_id] CSeq: 1 INVITE Contact: sip:sipp@[local_ip]:[local_port] Max-Forwards: 70 Subject: Performance Test Content-Type: application/sdp Content-Length: [len] v=0 o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip] s=- c=IN IP[media_ip_type] [media_ip] t=0 0 m=audio [media_port] RTP/AVP 0 a=rtpmap:0 PCMU/8000 ]]> ;tag=[pid]SIPpTag00[call_number] To: sut [peer_tag_param] Call-ID: [call_id] CSeq: 1 ACK Contact: sip:sipp@[local_ip]:[local_port] Max-Forwards: 70 Subject: Performance Test Content-Length: 0 ]]> Content-Length: 0 ]]> asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/calls/uas-blind-transfer.xml000066400000000000000000000071661242304700500265320ustar00rootroot00000000000000 Content-Length: 0 ]]> Content-Type: application/sdp Content-Length: [len] v=0 o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip] s=- c=IN IP[media_ip_type] [media_ip] t=0 0 m=audio [media_port] RTP/AVP 0 a=rtpmap:0 PCMU/8000 ]]> ;tag=[pid]SIPpTag00[call_number] To: sut [$remote_tag] Call-ID: [call_id] CSeq: 2 REFER Contact: sip:sipp@[local_ip]:[local_port] Max-Forwards: 70 Subject: Performance Test Refer-To: sip:[transfer_target]@[remote_ip]:[remote_port];user=phone Referred-By: sip:sipp@[local_ip]:[local_port] Diversion: ;reason="send_to_vm" Content-Length: 0 ]]> Content-Length: 0 ]]> Content-Length: 0 ]]> ;tag=[pid]SIPpTag00[call_number] To: sut [peer_tag_param] Call-ID: [call_id] CSeq: [cseq] BYE Contact: sip:sipp@[local_ip]:[local_port] Max-Forwards: 70 Subject: Performance Test Content-Length: 0 ]]> asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/calls/uas-hangup.xml000066400000000000000000000064631242304700500251010ustar00rootroot00000000000000 Content-Length: 0 ]]> Content-Type: application/sdp Content-Length: [len] v=0 o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip] s=- c=IN IP[media_ip_type] [media_ip] t=0 0 m=audio [media_port] RTP/AVP 0 a=rtpmap:0 PCMU/8000 ]]> ;tag=[pid]SIPpTag01[call_number] To: sut [$remote_tag] [last_Call-ID:] CSeq: [cseq] BYE Contact: sip:sipp@[local_ip]:[local_port] Max-Forwards: 70 Subject: Performance Test Content-Length: 0 ]]> asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/calls/uas-no-hangup.xml000066400000000000000000000030451242304700500255040ustar00rootroot00000000000000 Content-Length: 0 ]]> Content-Type: application/sdp Content-Length: [len] v=0 o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip] s=- c=IN IP[media_ip_type] [media_ip] t=0 0 m=audio [media_port] RTP/AVP 0 a=rtpmap:0 PCMU/8000 ]]> Content-Length: 0 ]]> asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/calls/uas-redirect.xml000066400000000000000000000014171242304700500254120ustar00rootroot00000000000000 Diversion: ;reason="send_to_vm" Content-Length: 0 ]]> asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/registration/000077500000000000000000000000001242304700500237125ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/registration/uac-register.xml000066400000000000000000000011751242304700500270320ustar00rootroot00000000000000 ;tag=[pid]SIPpTag00[call_number] To: [aor] Call-ID: [call_id] CSeq: 1 REGISTER Contact: [contact] Max-Forwards: 70 Subject: Performance Test Expires: [expires] Content-Length: 0 ]]> asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/registration/uac-unregister.xml000066400000000000000000000011651242304700500273740ustar00rootroot00000000000000 ;tag=[pid]SIPpTag00[call_number] To: [aor] Call-ID: [call_id] CSeq: 1 REGISTER Contact: [contact] Max-Forwards: 70 Subject: Performance Test Expires: 0 Content-Length: 0 ]]> asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/subscription/000077500000000000000000000000001242304700500237245ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/subscription/uac-subscribe-no-unsubscribe.xml000066400000000000000000000043761242304700500321430ustar00rootroot00000000000000 ;tag=[pid]SIPpTag00[call_number] To: sut Call-ID: [call_id] CSeq: 1 SUBSCRIBE Contact: sip:sipp@[local_ip]:[local_port] Max-Forwards: 70 Subject: Performance Test Event: [event] Accept: [accept] Expires: [expires] Content-Length: 0 ]]> Content-Length: 0 ]]> Content-Length: 0 ]]> asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/subscription/uac-subscribe-unsubscribe.xml000066400000000000000000000055641242304700500315310ustar00rootroot00000000000000 ;tag=[pid]SIPpTag00[call_number] To: sut Call-ID: [call_id] CSeq: 1 SUBSCRIBE Contact: sip:sipp@[local_ip]:[local_port] Max-Forwards: 70 Subject: Performance Test Event: [event] Accept: [accept] Expires: [expires] Content-Length: 0 ]]> Content-Length: 0 ]]> ;tag=[pid]SIPpTag00[call_number] To: sut [peer_tag_param] Call-ID: [call_id] CSeq: [cseq] SUBSCRIBE Contact: sip:sipp@[local_ip]:[local_port] Max-Forwards: 70 Subject: Performance Test Event: [event] Accept: [accept] Expires: 0 Content-Length: 0 ]]> Content-Length: 0 ]]> asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/table_of_contents000066400000000000000000000125211242304700500246140ustar00rootroot00000000000000Included here are common SIPp scenarios used for testing Asterisk. The scenarios are grouped based on their nature. Each scenario's name starts with either "uas" or "uac" to indicate the role the scenario plays. Below are descriptions of each of the scenarios. ========================================================= | CALLS | ========================================================= * uac-hangup.xml This scenario closely mirrors the default UAC scenario that SIPp uses. The only modification that has been made is to optionally accept a 181 response during call setup. The scenario calls a destination, waits a set amount of time and then sends a BYE. The destination can be specified with the -s option, and the waiting period can be specified with the -d option. * uac-no-hangup.xml This scenario is identical to uac-hangup.xml except that it does not initiate the hangup. Instead, it waits for a BYE to be sent to it, therefore the -d option will have no effect when this scenario runs. * uac-blind-transfer.xml This scenario establishes a dialog, waits a period of time and then sends a REFER to blind transfer the call. The scenario expects to receive a 202 response to the REFER. After this, it expects two NOTIFY requests, to which it responds with 200 OKs. After this, the scenario sends a BYE. The -s option can be used to control the destination of the original INVITE. The -d option can be used to control the waiting period. Use the -key option to set the "transfer_target" to the destination of the transfer. * uas-no-hangup.xml This scenario is the default SIPp UAS sceanrio. It answers and incoming INVITE with a 200 OK, awaits an ACK, and then awaits a BYE. * uas-hangup.xml This scenario is identical to uas-no-hangup.xml except that it sends a BYE instead of waiting for one. After the initial INVITE transaction completes, the scenario will wait and then transmit a BYE. The waiting period can be adjusted with the -d option. * uas-redirect.xml This scenario awaits an INVITE and then redirects the incoming call to a new target using a 302 response. The target of the redirect is controlled using the -key option to set the "redir_target". * uas-blind-transfer.xml This scenario completes an INVITE transaction, waits a period of time and then sends a REFER to blind transfer the call. The scenario expects to receive a 202 response to the REFER. After this, it expects two NOTIFY requests, to which it responds with 200 OKs. After this, the scenario sends a BYE. The -d option can be used to control the waiting period. Use the -key option to set the "transfer_target" to the destination of the transfer. ========================================================= | SUBSCRIPTION | ========================================================= * uac-subscribe-unsubscribe.xml This scenario establishes a subscription. There is a commented out portion where the user of this scenario may insert any NOTIFY transactions required for the scenario. After any NOTIFYs transactions are completed, the scenario will unsubscribe by sending a SUBSCRIBE with Expires header set to 0. Use the -s option to set the resource to subscribe to. Use the -key option to set the "event", "accept", and "expires" for the initial SUBSCRIBE to appropriate values. * uac-subscribe-no-unsubscribe.xml This scenario is the same as uac-subscribe-unsubscribe.xml except that this scenario expects the server to terminate the subscription by sending a NOTIFY. ========================================================= | REGISTRATION | ========================================================= * uac-register.xml This scenario sends a REGISTER and expects a 200 OK in return. Use the -s option to control the request URI of the REGISTER. Use the -k option to set the "aor", the "contact", and "expires" for the REGISTER. The "aor" and "contact" must be SIP URIs. * uac-register-unregister.xml This scenario is identical to uac-register.xml, except that the Expires header is set to 0. ========================================================= | TRANSFER | ========================================================= * referee.xml This scenario waits for a command from a cooperating SIPp scenario (referer.xml or referer_uas.xml), sends an INVITE, expects a 200 OK in return, sends an ACK to complete the transaction, and then relays call information to a the cooperating SIPp scenario. Use the -3pcc option to control the 3PCC communication port. The value associated with the -3pcc option should be the same for both scenarios (e.g. '-3pcc 127.0.0.1:5064'). * referer.xml This scenario functions as a cooperating counterpart for referee.xml and will initiate its call first. Once its call is up, it will send a command to referee.xml, collect its call information in a return command, and initiate the REFER attended transfer. This scenario MUST be started before referee.xml. * referer_uas.xml This scenario functions as a cooperating counterpart for referee.xml and will accept a call. Once its call is up, it will send a command to referee.xml, collect its call information in a return command, and initiate the REFER attended transfer. This scenario MUST be started before referee.xml. asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/transfer/000077500000000000000000000000001242304700500230245ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/transfer/referee.xml000066400000000000000000000074631242304700500251750ustar00rootroot00000000000000 ;tag=[call_number] To: Call-ID: [call_id] CSeq: [cseq] INVITE Contact: Max-Forwards: 70 Content-Type: application/sdp Content-Length: [len] v=0 o=- 1324901698 1324901698 IN IP[local_ip_type] [local_ip] s=- c=IN IP[media_ip_type] [media_ip] t=0 0 m=audio [media_port] RTP/AVP 0 101 a=sendrecv a=rtpmap:0 PCMU/8000 a=rtpmap:101 telephone-event/8000 ]]> Content-Length:0 ]]> asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/transfer/referer.xml000066400000000000000000000122071242304700500252020ustar00rootroot00000000000000 ;tag=[call_number] To: Call-ID: [call_id] CSeq: [cseq] INVITE Contact: Content-Type: application/sdp Max-Forwards: 70 Content-Length: [len] v=0 o=- 1324901698 1324901698 IN IP[local_ip_type] [local_ip] s=- c=IN IP[media_ip_type] [media_ip] t=0 0 m=audio [media_port] RTP/AVP 0 101 a=sendrecv a=rtpmap:0 PCMU/8000 a=rtpmap:101 telephone-event/8000 ]]> Max-Forwards: 70 Refer-to: <[$remote_contact]?Replaces=REMOTE[$callid_chunk_1]%40[$callid_chunk_2]%3Bto-tag%3D[$remote_to_tag]%3Bfrom-tag%3D[$remote_from_tag]> Referred-By: sip:bob@[local_ip] Content-Length: 0 ]]> Content-Length:0 ]]> Content-Length:0 ]]> ;tag=[call_number] To: [peer_tag_param] [last_Call-ID:] CSeq: [cseq] BYE Contact: sip:bob@[local_ip]:[local_port] Max-Forwards: 70 Subject: Performance Test Content-Length: 0 ]]> asterisk-testsuite-0.0.0+svn.5781/contrib/sipp/transfer/referer_uas.xml000066400000000000000000000133711242304700500260550ustar00rootroot00000000000000 Content-Type: application/sdp Content-Length: [len] v=0 o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip] s=- c=IN IP[media_ip_type] [media_ip] t=0 0 m=audio [media_port] RTP/AVP 0 a=rtpmap:0 PCMU/8000 ]]> Content-Type: application/sdp Max-Forwards: 70 Content-Length: [len] v=0 o=- 1324901698 1324901698 IN IP[local_ip_type] [local_ip] s=- c=IN IP[media_ip_type] [media_ip] t=0 0 m=audio [media_port] RTP/AVP 0 101 a=sendonly a=rtpmap:0 PCMU/8000 a=rtpmap:101 telephone-event/8000 ]]> Max-Forwards: 70 Refer-to: <[$remote_contact]?Replaces=REMOTE[call_id]%3Bto-tag%3D[$remote_to_tag]%3Bfrom-tag%3D[$remote_from_tag]> Referred-By: sip:bob@[local_ip] Content-Length: 0 ]]> Content-Length:0 ]]> asterisk-testsuite-0.0.0+svn.5781/contrib/testsuite-doxygen000066400000000000000000001777631242304700500237010ustar00rootroot00000000000000# Doxyfile 1.6.3 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project # # All text after a hash (#) is considered a comment and will be ignored # The format is: # TAG = value [value, ...] # For lists items can also be appended using: # TAG += value [value, ...] # Values that contain spaces should be placed between quotes (" ") #--------------------------------------------------------------------------- # Project related configuration options #--------------------------------------------------------------------------- # This tag specifies the encoding used for all characters in the config file # that follow. The default is UTF-8 which is also the encoding used for all # text before the first occurrence of this tag. Doxygen uses libiconv (or the # iconv built into libc) for the transcoding. See # http://www.gnu.org/software/libiconv for the list of possible encodings. DOXYFILE_ENCODING = UTF-8 # The PROJECT_NAME tag is a single word (or a sequence of words surrounded # by quotes) that should identify the project. PROJECT_NAME = "Asterisk Testsuite" # The PROJECT_NUMBER tag can be used to enter a project or revision number. # This could be handy for archiving the generated documentation or # if some version control system is used. PROJECT_NUMBER = # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) # base path where the generated documentation will be put. # If a relative path is entered, it will be relative to the location # where doxygen was started. If left blank the current directory will be used. OUTPUT_DIRECTORY = doc/api # If the CREATE_SUBDIRS tag is set to YES, then doxygen will create # 4096 sub-directories (in 2 levels) under the output directory of each output # format and will distribute the generated files over these directories. # Enabling this option can be useful when feeding doxygen a huge amount of # source files, where putting all generated files in the same directory would # otherwise cause performance problems for the file system. CREATE_SUBDIRS = NO # The OUTPUT_LANGUAGE tag is used to specify the language in which all # documentation generated by doxygen is written. Doxygen will use this # information to generate all constant output in the proper language. # The default language is English, other supported languages are: # Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, # Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German, # Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English # messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, # Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrilic, Slovak, # Slovene, Spanish, Swedish, Ukrainian, and Vietnamese. OUTPUT_LANGUAGE = English # If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will # include brief member descriptions after the members that are listed in # the file and class documentation (similar to JavaDoc). # Set to NO to disable this. BRIEF_MEMBER_DESC = YES # If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend # the brief description of a member or function before the detailed description. # Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the # brief descriptions will be completely suppressed. REPEAT_BRIEF = YES # This tag implements a quasi-intelligent brief description abbreviator # that is used to form the text in various listings. Each string # in this list, if found as the leading text of the brief description, will be # stripped from the text and the result after processing the whole list, is # used as the annotated text. Otherwise, the brief description is used as-is. # If left blank, the following values are used ("$name" is automatically # replaced with the name of the entity): "The $name class" "The $name widget" # "The $name file" "is" "provides" "specifies" "contains" # "represents" "a" "an" "the" ABBREVIATE_BRIEF = # If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then # Doxygen will generate a detailed section even if there is only a brief # description. ALWAYS_DETAILED_SEC = NO # If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all # inherited members of a class in the documentation of that class as if those # members were ordinary class members. Constructors, destructors and assignment # operators of the base classes will not be shown. INLINE_INHERITED_MEMB = NO # If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full # path before files name in the file list and in the header files. If set # to NO the shortest path that makes the file name unique will be used. FULL_PATH_NAMES = NO # If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag # can be used to strip a user-defined part of the path. Stripping is # only done if one of the specified strings matches the left-hand part of # the path. The tag can be used to show relative paths in the file list. # If left blank the directory from which doxygen is run is used as the # path to strip. STRIP_FROM_PATH = # The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of # the path mentioned in the documentation of a class, which tells # the reader which header file to include in order to use a class. # If left blank only the name of the header file containing the class # definition is used. Otherwise one should specify the include paths that # are normally passed to the compiler using the -I flag. STRIP_FROM_INC_PATH = # If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter # (but less readable) file names. This can be useful is your file systems # doesn't support long names like on DOS, Mac, or CD-ROM. SHORT_NAMES = NO # If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen # will interpret the first line (until the first dot) of a JavaDoc-style # comment as the brief description. If set to NO, the JavaDoc # comments will behave just like regular Qt-style comments # (thus requiring an explicit @brief command for a brief description.) JAVADOC_AUTOBRIEF = NO # If the QT_AUTOBRIEF tag is set to YES then Doxygen will # interpret the first line (until the first dot) of a Qt-style # comment as the brief description. If set to NO, the comments # will behave just like regular Qt-style comments (thus requiring # an explicit \brief command for a brief description.) QT_AUTOBRIEF = NO # The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen # treat a multi-line C++ special comment block (i.e. a block of //! or /// # comments) as a brief description. This used to be the default behaviour. # The new default is to treat a multi-line C++ comment block as a detailed # description. Set this tag to YES if you prefer the old behaviour instead. MULTILINE_CPP_IS_BRIEF = NO # If the INHERIT_DOCS tag is set to YES (the default) then an undocumented # member inherits the documentation from any documented member that it # re-implements. INHERIT_DOCS = YES # If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce # a new page for each member. If set to NO, the documentation of a member will # be part of the file/class/namespace that contains it. SEPARATE_MEMBER_PAGES = NO # The TAB_SIZE tag can be used to set the number of spaces in a tab. # Doxygen uses this value to replace tabs by spaces in code fragments. TAB_SIZE = 4 # This tag can be used to specify a number of aliases that acts # as commands in the documentation. An alias has the form "name=value". # For example adding "sideeffect=\par Side Effects:\n" will allow you to # put the command \sideeffect (or @sideeffect) in the documentation, which # will result in a user-defined paragraph with heading "Side Effects:". # You can put \n's in the value part of an alias to insert newlines. ALIASES = # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C # sources only. Doxygen will then generate output that is more tailored for C. # For instance, some of the names that are used will be different. The list # of all members will be omitted, etc. OPTIMIZE_OUTPUT_FOR_C = NO # Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java # sources only. Doxygen will then generate output that is more tailored for # Java. For instance, namespaces will be presented as packages, qualified # scopes will look different, etc. OPTIMIZE_OUTPUT_JAVA = YES # Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran # sources only. Doxygen will then generate output that is more tailored for # Fortran. OPTIMIZE_FOR_FORTRAN = NO # Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL # sources. Doxygen will then generate output that is tailored for # VHDL. OPTIMIZE_OUTPUT_VHDL = NO # Doxygen selects the parser to use depending on the extension of the files it parses. # With this tag you can assign which parser to use for a given extension. # Doxygen has a built-in mapping, but you can override or extend it using this tag. # The format is ext=language, where ext is a file extension, and language is one of # the parsers supported by doxygen: IDL, Java, Javascript, C#, C, C++, D, PHP, # Objective-C, Python, Fortran, VHDL, C, C++. For instance to make doxygen treat # .inc files as Fortran files (default is PHP), and .f files as C (default is Fortran), # use: inc=Fortran f=C. Note that for custom extensions you also need to set FILE_PATTERNS otherwise the files are not read by doxygen. EXTENSION_MAPPING = # If you use STL classes (i.e. std::string, std::vector, etc.) but do not want # to include (a tag file for) the STL sources as input, then you should # set this tag to YES in order to let doxygen match functions declarations and # definitions whose arguments contain STL classes (e.g. func(std::string); v.s. # func(std::string) {}). This also make the inheritance and collaboration # diagrams that involve STL classes more complete and accurate. BUILTIN_STL_SUPPORT = NO # If you use Microsoft's C++/CLI language, you should set this option to YES to # enable parsing support. CPP_CLI_SUPPORT = NO # Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. # Doxygen will parse them like normal C++ but will assume all classes use public # instead of private inheritance when no explicit protection keyword is present. SIP_SUPPORT = NO # For Microsoft's IDL there are propget and propput attributes to indicate getter # and setter methods for a property. Setting this option to YES (the default) # will make doxygen to replace the get and set methods by a property in the # documentation. This will only work if the methods are indeed getting or # setting a simple type. If this is not the case, or you want to show the # methods anyway, you should set this option to NO. IDL_PROPERTY_SUPPORT = YES # If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC # tag is set to YES, then doxygen will reuse the documentation of the first # member in the group (if any) for the other members of the group. By default # all members of a group must be documented explicitly. DISTRIBUTE_GROUP_DOC = NO # Set the SUBGROUPING tag to YES (the default) to allow class member groups of # the same type (for instance a group of public functions) to be put as a # subgroup of that type (e.g. under the Public Functions section). Set it to # NO to prevent subgrouping. Alternatively, this can be done per class using # the \nosubgrouping command. SUBGROUPING = YES # When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum # is documented as struct, union, or enum with the name of the typedef. So # typedef struct TypeS {} TypeT, will appear in the documentation as a struct # with name TypeT. When disabled the typedef will appear as a member of a file, # namespace, or class. And the struct will be named TypeS. This can typically # be useful for C code in case the coding convention dictates that all compound # types are typedef'ed and only the typedef is referenced, never the tag name. TYPEDEF_HIDES_STRUCT = NO # The SYMBOL_CACHE_SIZE determines the size of the internal cache use to # determine which symbols to keep in memory and which to flush to disk. # When the cache is full, less often used symbols will be written to disk. # For small to medium size projects (<1000 input files) the default value is # probably good enough. For larger projects a too small cache size can cause # doxygen to be busy swapping symbols to and from disk most of the time # causing a significant performance penality. # If the system has enough physical memory increasing the cache will improve the # performance by keeping more symbols in memory. Note that the value works on # a logarithmic scale so increasing the size by one will rougly double the # memory usage. The cache size is given by this formula: # 2^(16+SYMBOL_CACHE_SIZE). The valid range is 0..9, the default is 0, # corresponding to a cache size of 2^16 = 65536 symbols SYMBOL_CACHE_SIZE = 0 #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- # If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in # documentation are documented, even if no documentation was available. # Private class members and static file members will be hidden unless # the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES EXTRACT_ALL = YES # If the EXTRACT_PRIVATE tag is set to YES all private members of a class # will be included in the documentation. EXTRACT_PRIVATE = NO # If the EXTRACT_STATIC tag is set to YES all static members of a file # will be included in the documentation. EXTRACT_STATIC = YES # If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) # defined locally in source files will be included in the documentation. # If set to NO only classes defined in header files are included. EXTRACT_LOCAL_CLASSES = YES # This flag is only useful for Objective-C code. When set to YES local # methods, which are defined in the implementation section but not in # the interface are included in the documentation. # If set to NO (the default) only methods in the interface are included. EXTRACT_LOCAL_METHODS = NO # If this flag is set to YES, the members of anonymous namespaces will be # extracted and appear in the documentation as a namespace called # 'anonymous_namespace{file}', where file will be replaced with the base # name of the file that contains the anonymous namespace. By default # anonymous namespace are hidden. EXTRACT_ANON_NSPACES = NO # If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all # undocumented members of documented classes, files or namespaces. # If set to NO (the default) these members will be included in the # various overviews, but no documentation section is generated. # This option has no effect if EXTRACT_ALL is enabled. HIDE_UNDOC_MEMBERS = NO # If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all # undocumented classes that are normally visible in the class hierarchy. # If set to NO (the default) these classes will be included in the various # overviews. This option has no effect if EXTRACT_ALL is enabled. HIDE_UNDOC_CLASSES = NO # If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all # friend (class|struct|union) declarations. # If set to NO (the default) these declarations will be included in the # documentation. HIDE_FRIEND_COMPOUNDS = NO # If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any # documentation blocks found inside the body of a function. # If set to NO (the default) these blocks will be appended to the # function's detailed documentation block. HIDE_IN_BODY_DOCS = NO # The INTERNAL_DOCS tag determines if documentation # that is typed after a \internal command is included. If the tag is set # to NO (the default) then the documentation will be excluded. # Set it to YES to include the internal documentation. INTERNAL_DOCS = NO # If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate # file names in lower-case letters. If set to YES upper-case letters are also # allowed. This is useful if you have classes or files whose names only differ # in case and if your file system supports case sensitive file names. Windows # and Mac users are advised to set this option to NO. CASE_SENSE_NAMES = YES # If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen # will show members with their full class and namespace scopes in the # documentation. If set to YES the scope will be hidden. HIDE_SCOPE_NAMES = NO # If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen # will put a list of the files that are included by a file in the documentation # of that file. SHOW_INCLUDE_FILES = YES # If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen # will list include files with double quotes in the documentation # rather than with sharp brackets. FORCE_LOCAL_INCLUDES = NO # If the INLINE_INFO tag is set to YES (the default) then a tag [inline] # is inserted in the documentation for inline members. INLINE_INFO = YES # If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen # will sort the (detailed) documentation of file and class members # alphabetically by member name. If set to NO the members will appear in # declaration order. SORT_MEMBER_DOCS = YES # If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the # brief documentation of file, namespace and class members alphabetically # by member name. If set to NO (the default) the members will appear in # declaration order. SORT_BRIEF_DOCS = NO # If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the (brief and detailed) documentation of class members so that constructors and destructors are listed first. If set to NO (the default) the constructors will appear in the respective orders defined by SORT_MEMBER_DOCS and SORT_BRIEF_DOCS. This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO. SORT_MEMBERS_CTORS_1ST = NO # If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the # hierarchy of group names into alphabetical order. If set to NO (the default) # the group names will appear in their defined order. SORT_GROUP_NAMES = NO # If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be # sorted by fully-qualified names, including namespaces. If set to # NO (the default), the class list will be sorted only by class name, # not including the namespace part. # Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. # Note: This option applies only to the class list, not to the # alphabetical list. SORT_BY_SCOPE_NAME = NO # The GENERATE_TODOLIST tag can be used to enable (YES) or # disable (NO) the todo list. This list is created by putting \todo # commands in the documentation. GENERATE_TODOLIST = YES # The GENERATE_TESTLIST tag can be used to enable (YES) or # disable (NO) the test list. This list is created by putting \test # commands in the documentation. GENERATE_TESTLIST = YES # The GENERATE_BUGLIST tag can be used to enable (YES) or # disable (NO) the bug list. This list is created by putting \bug # commands in the documentation. GENERATE_BUGLIST = YES # The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or # disable (NO) the deprecated list. This list is created by putting # \deprecated commands in the documentation. GENERATE_DEPRECATEDLIST= YES # The ENABLED_SECTIONS tag can be used to enable conditional # documentation sections, marked by \if sectionname ... \endif. ENABLED_SECTIONS = # The MAX_INITIALIZER_LINES tag determines the maximum number of lines # the initial value of a variable or define consists of for it to appear in # the documentation. If the initializer consists of more lines than specified # here it will be hidden. Use a value of 0 to hide initializers completely. # The appearance of the initializer of individual variables and defines in the # documentation can be controlled using \showinitializer or \hideinitializer # command in the documentation regardless of this setting. MAX_INITIALIZER_LINES = 30 # Set the SHOW_USED_FILES tag to NO to disable the list of files generated # at the bottom of the documentation of classes and structs. If set to YES the # list will mention the files that were used to generate the documentation. SHOW_USED_FILES = YES # If the sources in your project are distributed over multiple directories # then setting the SHOW_DIRECTORIES tag to YES will show the directory hierarchy # in the documentation. The default is NO. SHOW_DIRECTORIES = YES # Set the SHOW_FILES tag to NO to disable the generation of the Files page. # This will remove the Files entry from the Quick Index and from the # Folder Tree View (if specified). The default is YES. SHOW_FILES = YES # Set the SHOW_NAMESPACES tag to NO to disable the generation of the # Namespaces page. # This will remove the Namespaces entry from the Quick Index # and from the Folder Tree View (if specified). The default is YES. SHOW_NAMESPACES = YES # The FILE_VERSION_FILTER tag can be used to specify a program or script that # doxygen should invoke to get the current version for each file (typically from # the version control system). Doxygen will invoke the program by executing (via # popen()) the command , where is the value of # the FILE_VERSION_FILTER tag, and is the name of an input file # provided by doxygen. Whatever the program writes to standard output # is used as the file version. See the manual for examples. FILE_VERSION_FILTER = # The LAYOUT_FILE tag can be used to specify a layout file which will be parsed by # doxygen. The layout file controls the global structure of the generated output files # in an output format independent way. The create the layout file that represents # doxygen's defaults, run doxygen with the -l option. You can optionally specify a # file name after the option, if omitted DoxygenLayout.xml will be used as the name # of the layout file. LAYOUT_FILE = #--------------------------------------------------------------------------- # configuration options related to warning and progress messages #--------------------------------------------------------------------------- # The QUIET tag can be used to turn on/off the messages that are generated # by doxygen. Possible values are YES and NO. If left blank NO is used. QUIET = NO # The WARNINGS tag can be used to turn on/off the warning messages that are # generated by doxygen. Possible values are YES and NO. If left blank # NO is used. WARNINGS = YES # If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings # for undocumented members. If EXTRACT_ALL is set to YES then this flag will # automatically be disabled. WARN_IF_UNDOCUMENTED = YES # If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for # potential errors in the documentation, such as not documenting some # parameters in a documented function, or documenting parameters that # don't exist or using markup commands wrongly. WARN_IF_DOC_ERROR = YES # This WARN_NO_PARAMDOC option can be abled to get warnings for # functions that are documented, but have no documentation for their parameters # or return value. If set to NO (the default) doxygen will only warn about # wrong or incomplete parameter documentation, but not about the absence of # documentation. WARN_NO_PARAMDOC = NO # The WARN_FORMAT tag determines the format of the warning messages that # doxygen can produce. The string should contain the $file, $line, and $text # tags, which will be replaced by the file and line number from which the # warning originated and the warning text. Optionally the format may contain # $version, which will be replaced by the version of the file (if it could # be obtained via FILE_VERSION_FILTER) WARN_FORMAT = "$file:$line: $text" # The WARN_LOGFILE tag can be used to specify a file to which warning # and error messages should be written. If left blank the output is written # to stderr. WARN_LOGFILE = #--------------------------------------------------------------------------- # configuration options related to the input files #--------------------------------------------------------------------------- # The INPUT tag can be used to specify the files and/or directories that contain # documented source files. You may enter file names like "myfile.cpp" or # directories like "/usr/src/myproject". Separate the files or directories # with spaces. INPUT = ./ \ lib/python \ lib/python/asterisk \ lib/python/sipp # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is # also the default input encoding. Doxygen uses libiconv (or the iconv built # into libc) for the transcoding. See http://www.gnu.org/software/libiconv for # the list of possible encodings. INPUT_ENCODING = UTF-8 # If the value of the INPUT tag contains directories, you can use the # FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp # and *.h) to filter out the source-files in the directories. If left # blank the following patterns are tested: # *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx # *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.py *.f90 FILE_PATTERNS = *.py # The RECURSIVE tag can be used to turn specify whether or not subdirectories # should be searched for input files as well. Possible values are YES and NO. # If left blank NO is used. RECURSIVE = NO # The EXCLUDE tag can be used to specify files and/or directories that should # excluded from the INPUT source files. This way you can easily exclude a # subdirectory from a directory tree whose root is specified with the INPUT tag. EXCLUDE = # The EXCLUDE_SYMLINKS tag can be used select whether or not files or # directories that are symbolic links (a Unix filesystem feature) are excluded # from the input. EXCLUDE_SYMLINKS = NO # If the value of the INPUT tag contains directories, you can use the # EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude # certain files from those directories. Note that the wildcards are matched # against the file with absolute path, so to exclude all test directories # for example use the pattern */test/* EXCLUDE_PATTERNS = # The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names # (namespaces, classes, functions, etc.) that should be excluded from the # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, # AClass::ANamespace, ANamespace::*Test EXCLUDE_SYMBOLS = # The EXAMPLE_PATH tag can be used to specify one or more files or # directories that contain example code fragments that are included (see # the \include command). EXAMPLE_PATH = ./ \ doc \ configs # If the value of the EXAMPLE_PATH tag contains directories, you can use the # EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp # and *.h) to filter out the source-files in the directories. If left # blank all files are included. EXAMPLE_PATTERNS = # If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be # searched for input files to be used with the \include or \dontinclude # commands irrespective of the value of the RECURSIVE tag. # Possible values are YES and NO. If left blank NO is used. EXAMPLE_RECURSIVE = NO # The IMAGE_PATH tag can be used to specify one or more files or # directories that contain image that are included in the documentation (see # the \image command). IMAGE_PATH = # The INPUT_FILTER tag can be used to specify a program that doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program # by executing (via popen()) the command , where # is the value of the INPUT_FILTER tag, and is the name of an # input file. Doxygen will then use the output that the filter program writes # to standard output. # If FILTER_PATTERNS is specified, this tag will be # ignored. INPUT_FILTER = # The FILTER_PATTERNS tag can be used to specify filters on a per file pattern # basis. # Doxygen will compare the file name with each pattern and apply the # filter if there is a match. # The filters are a list of the form: # pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further # info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER # is applied to all files. FILTER_PATTERNS = # If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using # INPUT_FILTER) will be used to filter the input files when producing source # files to browse (i.e. when SOURCE_BROWSER is set to YES). FILTER_SOURCE_FILES = NO #--------------------------------------------------------------------------- # configuration options related to source browsing #--------------------------------------------------------------------------- # If the SOURCE_BROWSER tag is set to YES then a list of source files will # be generated. Documented entities will be cross-referenced with these sources. # Note: To get rid of all source code in the generated output, make sure also # VERBATIM_HEADERS is set to NO. SOURCE_BROWSER = YES # Setting the INLINE_SOURCES tag to YES will include the body # of functions and classes directly in the documentation. INLINE_SOURCES = YES # Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct # doxygen to hide any special comment blocks from generated source code # fragments. Normal C and C++ comments will always remain visible. STRIP_CODE_COMMENTS = NO # If the REFERENCED_BY_RELATION tag is set to YES # then for each documented function all documented # functions referencing it will be listed. REFERENCED_BY_RELATION = YES # If the REFERENCES_RELATION tag is set to YES # then for each documented function all documented entities # called/used by that function will be listed. REFERENCES_RELATION = YES # If the REFERENCES_LINK_SOURCE tag is set to YES (the default) # and SOURCE_BROWSER tag is set to YES, then the hyperlinks from # functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will # link to the source code. # Otherwise they will link to the documentation. REFERENCES_LINK_SOURCE = YES # If the USE_HTAGS tag is set to YES then the references to source code # will point to the HTML generated by the htags(1) tool instead of doxygen # built-in source browser. The htags tool is part of GNU's global source # tagging system (see http://www.gnu.org/software/global/global.html). You # will need version 4.8.6 or higher. USE_HTAGS = NO # If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen # will generate a verbatim copy of the header file for each class for # which an include is specified. Set to NO to disable this. VERBATIM_HEADERS = YES #--------------------------------------------------------------------------- # configuration options related to the alphabetical class index #--------------------------------------------------------------------------- # If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index # of all compounds will be generated. Enable this if the project # contains a lot of classes, structs, unions or interfaces. ALPHABETICAL_INDEX = YES # If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then # the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns # in which this list will be split (can be a number in the range [1..20]) COLS_IN_ALPHA_INDEX = 5 # In case all classes in a project start with a common prefix, all # classes will be put under the same header in the alphabetical index. # The IGNORE_PREFIX tag can be used to specify one or more prefixes that # should be ignored while generating the index headers. IGNORE_PREFIX = #--------------------------------------------------------------------------- # configuration options related to the HTML output #--------------------------------------------------------------------------- # If the GENERATE_HTML tag is set to YES (the default) Doxygen will # generate HTML output. GENERATE_HTML = YES # The HTML_OUTPUT tag is used to specify where the HTML docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `html' will be used as the default path. HTML_OUTPUT = html # The HTML_FILE_EXTENSION tag can be used to specify the file extension for # each generated HTML page (for example: .htm,.php,.asp). If it is left blank # doxygen will generate files with .html extension. HTML_FILE_EXTENSION = .html # The HTML_HEADER tag can be used to specify a personal HTML header for # each generated HTML page. If it is left blank doxygen will generate a # standard header. HTML_HEADER = # The HTML_FOOTER tag can be used to specify a personal HTML footer for # each generated HTML page. If it is left blank doxygen will generate a # standard footer. HTML_FOOTER = # The HTML_STYLESHEET tag can be used to specify a user-defined cascading # style sheet that is used by each HTML page. It can be used to # fine-tune the look of the HTML output. If the tag is left blank doxygen # will generate a default style sheet. Note that doxygen will try to copy # the style sheet file to the HTML output directory, so don't put your own # stylesheet in the HTML output directory as well, or it will be erased! HTML_STYLESHEET = # If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML # page will contain the date and time when the page was generated. Setting # this to NO can help when comparing the output of multiple runs. HTML_TIMESTAMP = YES # If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes, # files or namespaces will be aligned in HTML using tables. If set to # NO a bullet list will be used. HTML_ALIGN_MEMBERS = YES # If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML # documentation will contain sections that can be hidden and shown after the # page has loaded. For this to work a browser that supports # JavaScript and DHTML is required (for instance Mozilla 1.0+, Firefox # Netscape 6.0+, Internet explorer 5.0+, Konqueror, or Safari). HTML_DYNAMIC_SECTIONS = NO # If the GENERATE_DOCSET tag is set to YES, additional index files # will be generated that can be used as input for Apple's Xcode 3 # integrated development environment, introduced with OSX 10.5 (Leopard). # To create a documentation set, doxygen will generate a Makefile in the # HTML output directory. Running make will produce the docset in that # directory and running "make install" will install the docset in # ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find # it at startup. # See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html for more information. GENERATE_DOCSET = NO # When GENERATE_DOCSET tag is set to YES, this tag determines the name of the # feed. A documentation feed provides an umbrella under which multiple # documentation sets from a single provider (such as a company or product suite) # can be grouped. DOCSET_FEEDNAME = "Doxygen generated docs" # When GENERATE_DOCSET tag is set to YES, this tag specifies a string that # should uniquely identify the documentation set bundle. This should be a # reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen # will append .docset to the name. DOCSET_BUNDLE_ID = org.doxygen.Project # If the GENERATE_HTMLHELP tag is set to YES, additional index files # will be generated that can be used as input for tools like the # Microsoft HTML help workshop to generate a compiled HTML help file (.chm) # of the generated HTML documentation. GENERATE_HTMLHELP = NO # If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can # be used to specify the file name of the resulting .chm file. You # can add a path in front of the file if the result should not be # written to the html output directory. CHM_FILE = # If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can # be used to specify the location (absolute path including file name) of # the HTML help compiler (hhc.exe). If non-empty doxygen will try to run # the HTML help compiler on the generated index.hhp. HHC_LOCATION = # If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag # controls if a separate .chi index file is generated (YES) or that # it should be included in the master .chm file (NO). GENERATE_CHI = NO # If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING # is used to encode HtmlHelp index (hhk), content (hhc) and project file # content. CHM_INDEX_ENCODING = # If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag # controls whether a binary table of contents is generated (YES) or a # normal table of contents (NO) in the .chm file. BINARY_TOC = NO # The TOC_EXPAND flag can be set to YES to add extra items for group members # to the contents of the HTML help documentation and to the tree view. TOC_EXPAND = NO # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and QHP_VIRTUAL_FOLDER # are set, an additional index file will be generated that can be used as input for # Qt's qhelpgenerator to generate a Qt Compressed Help (.qch) of the generated # HTML documentation. GENERATE_QHP = NO # If the QHG_LOCATION tag is specified, the QCH_FILE tag can # be used to specify the file name of the resulting .qch file. # The path specified is relative to the HTML output folder. QCH_FILE = # The QHP_NAMESPACE tag specifies the namespace to use when generating # Qt Help Project output. For more information please see # http://doc.trolltech.com/qthelpproject.html#namespace QHP_NAMESPACE = org.doxygen.Project # The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating # Qt Help Project output. For more information please see # http://doc.trolltech.com/qthelpproject.html#virtual-folders QHP_VIRTUAL_FOLDER = doc # If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to add. # For more information please see # http://doc.trolltech.com/qthelpproject.html#custom-filters QHP_CUST_FILTER_NAME = # The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the custom filter to add.For more information please see # Qt Help Project / Custom Filters. QHP_CUST_FILTER_ATTRS = # The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this project's # filter section matches. # Qt Help Project / Filter Attributes. QHP_SECT_FILTER_ATTRS = # If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can # be used to specify the location of Qt's qhelpgenerator. # If non-empty doxygen will try to run qhelpgenerator on the generated # .qhp file. QHG_LOCATION = # If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files # will be generated, which together with the HTML files, form an Eclipse help # plugin. To install this plugin and make it available under the help contents # menu in Eclipse, the contents of the directory containing the HTML and XML # files needs to be copied into the plugins directory of eclipse. The name of # the directory within the plugins directory should be the same as # the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before the help appears. GENERATE_ECLIPSEHELP = NO # A unique identifier for the eclipse help plugin. When installing the plugin # the directory name containing the HTML and XML files should also have # this name. ECLIPSE_DOC_ID = org.doxygen.Project # The DISABLE_INDEX tag can be used to turn on/off the condensed index at # top of each HTML page. The value NO (the default) enables the index and # the value YES disables it. DISABLE_INDEX = NO # This tag can be used to set the number of enum values (range [1..20]) # that doxygen will group on one line in the generated HTML documentation. ENUM_VALUES_PER_LINE = 4 # The GENERATE_TREEVIEW tag is used to specify whether a tree-like index # structure should be generated to display hierarchical information. # If the tag value is set to YES, a side panel will be generated # containing a tree-like index structure (just like the one that # is generated for HTML Help). For this to work a browser that supports # JavaScript, DHTML, CSS and frames is required (i.e. any modern browser). # Windows users are probably better off using the HTML help feature. GENERATE_TREEVIEW = YES # By enabling USE_INLINE_TREES, doxygen will generate the Groups, Directories, # and Class Hierarchy pages using a tree view instead of an ordered list. USE_INLINE_TREES = NO # If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be # used to set the initial width (in pixels) of the frame in which the tree # is shown. TREEVIEW_WIDTH = 250 # Use this tag to change the font size of Latex formulas included # as images in the HTML documentation. The default is 10. Note that # when you change the font size after a successful doxygen run you need # to manually remove any form_*.png images from the HTML output directory # to force them to be regenerated. FORMULA_FONTSIZE = 10 # When the SEARCHENGINE tag is enabled doxygen will generate a search box for the HTML output. The underlying search engine uses javascript # and DHTML and should work on any modern browser. Note that when using HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) there is already a search function so this one should # typically be disabled. For large projects the javascript based search engine # can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution. SEARCHENGINE = YES # When the SERVER_BASED_SEARCH tag is enabled the search engine will be implemented using a PHP enabled web server instead of at the web client using Javascript. Doxygen will generate the search PHP script and index # file to put on the web server. The advantage of the server based approach is that it scales better to large projects and allows full text search. The disadvances is that it is more difficult to setup # and does not have live searching capabilities. SERVER_BASED_SEARCH = NO #--------------------------------------------------------------------------- # configuration options related to the LaTeX output #--------------------------------------------------------------------------- # If the GENERATE_LATEX tag is set to YES (the default) Doxygen will # generate Latex output. GENERATE_LATEX = NO # The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `latex' will be used as the default path. LATEX_OUTPUT = latex # The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be # invoked. If left blank `latex' will be used as the default command name. # Note that when enabling USE_PDFLATEX this option is only used for # generating bitmaps for formulas in the HTML output, but not in the # Makefile that is written to the output directory. LATEX_CMD_NAME = latex # The MAKEINDEX_CMD_NAME tag can be used to specify the command name to # generate index for LaTeX. If left blank `makeindex' will be used as the # default command name. MAKEINDEX_CMD_NAME = makeindex # If the COMPACT_LATEX tag is set to YES Doxygen generates more compact # LaTeX documents. This may be useful for small projects and may help to # save some trees in general. COMPACT_LATEX = NO # The PAPER_TYPE tag can be used to set the paper type that is used # by the printer. Possible values are: a4, a4wide, letter, legal and # executive. If left blank a4wide will be used. PAPER_TYPE = a4wide # The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX # packages that should be included in the LaTeX output. EXTRA_PACKAGES = # The LATEX_HEADER tag can be used to specify a personal LaTeX header for # the generated latex document. The header should contain everything until # the first chapter. If it is left blank doxygen will generate a # standard header. Notice: only use this tag if you know what you are doing! LATEX_HEADER = # If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated # is prepared for conversion to pdf (using ps2pdf). The pdf file will # contain links (just like the HTML output) instead of page references # This makes the output suitable for online browsing using a pdf viewer. PDF_HYPERLINKS = # If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of # plain latex in the generated Makefile. Set this option to YES to get a # higher quality PDF documentation. USE_PDFLATEX = YES # If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. # command to the generated LaTeX files. This will instruct LaTeX to keep # running if errors occur, instead of asking the user for help. # This option is also used when generating formulas in HTML. LATEX_BATCHMODE = NO # If LATEX_HIDE_INDICES is set to YES then doxygen will not # include the index chapters (such as File Index, Compound Index, etc.) # in the output. LATEX_HIDE_INDICES = NO # If LATEX_SOURCE_CODE is set to YES then doxygen will include source code with syntax highlighting in the LaTeX output. Note that which sources are shown also depends on other settings such as SOURCE_BROWSER. LATEX_SOURCE_CODE = NO #--------------------------------------------------------------------------- # configuration options related to the RTF output #--------------------------------------------------------------------------- # If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output # The RTF output is optimized for Word 97 and may not look very pretty with # other RTF readers or editors. GENERATE_RTF = NO # The RTF_OUTPUT tag is used to specify where the RTF docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `rtf' will be used as the default path. RTF_OUTPUT = rtf # If the COMPACT_RTF tag is set to YES Doxygen generates more compact # RTF documents. This may be useful for small projects and may help to # save some trees in general. COMPACT_RTF = NO # If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated # will contain hyperlink fields. The RTF file will # contain links (just like the HTML output) instead of page references. # This makes the output suitable for online browsing using WORD or other # programs which support those fields. # Note: wordpad (write) and others do not support links. RTF_HYPERLINKS = NO # Load stylesheet definitions from file. Syntax is similar to doxygen's # config file, i.e. a series of assignments. You only have to provide # replacements, missing definitions are set to their default value. RTF_STYLESHEET_FILE = # Set optional variables used in the generation of an rtf document. # Syntax is similar to doxygen's config file. RTF_EXTENSIONS_FILE = #--------------------------------------------------------------------------- # configuration options related to the man page output #--------------------------------------------------------------------------- # If the GENERATE_MAN tag is set to YES (the default) Doxygen will # generate man pages GENERATE_MAN = NO # The MAN_OUTPUT tag is used to specify where the man pages will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `man' will be used as the default path. MAN_OUTPUT = man # The MAN_EXTENSION tag determines the extension that is added to # the generated man pages (default is the subroutine's section .3) MAN_EXTENSION = .3 # If the MAN_LINKS tag is set to YES and Doxygen generates man output, # then it will generate one additional man file for each entity # documented in the real man page(s). These additional files # only source the real man page, but without them the man command # would be unable to find the correct page. The default is NO. MAN_LINKS = NO #--------------------------------------------------------------------------- # configuration options related to the XML output #--------------------------------------------------------------------------- # If the GENERATE_XML tag is set to YES Doxygen will # generate an XML file that captures the structure of # the code including all documentation. GENERATE_XML = NO # The XML_OUTPUT tag is used to specify where the XML pages will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `xml' will be used as the default path. XML_OUTPUT = xml # The XML_SCHEMA tag can be used to specify an XML schema, # which can be used by a validating XML parser to check the # syntax of the XML files. XML_SCHEMA = # The XML_DTD tag can be used to specify an XML DTD, # which can be used by a validating XML parser to check the # syntax of the XML files. XML_DTD = # If the XML_PROGRAMLISTING tag is set to YES Doxygen will # dump the program listings (including syntax highlighting # and cross-referencing information) to the XML output. Note that # enabling this will significantly increase the size of the XML output. XML_PROGRAMLISTING = YES #--------------------------------------------------------------------------- # configuration options for the AutoGen Definitions output #--------------------------------------------------------------------------- # If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will # generate an AutoGen Definitions (see autogen.sf.net) file # that captures the structure of the code including all # documentation. Note that this feature is still experimental # and incomplete at the moment. GENERATE_AUTOGEN_DEF = NO #--------------------------------------------------------------------------- # configuration options related to the Perl module output #--------------------------------------------------------------------------- # If the GENERATE_PERLMOD tag is set to YES Doxygen will # generate a Perl module file that captures the structure of # the code including all documentation. Note that this # feature is still experimental and incomplete at the # moment. GENERATE_PERLMOD = NO # If the PERLMOD_LATEX tag is set to YES Doxygen will generate # the necessary Makefile rules, Perl scripts and LaTeX code to be able # to generate PDF and DVI output from the Perl module output. PERLMOD_LATEX = NO # If the PERLMOD_PRETTY tag is set to YES the Perl module output will be # nicely formatted so it can be parsed by a human reader. # This is useful # if you want to understand what is going on. # On the other hand, if this # tag is set to NO the size of the Perl module output will be much smaller # and Perl will parse it just the same. PERLMOD_PRETTY = YES # The names of the make variables in the generated doxyrules.make file # are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. # This is useful so different doxyrules.make files included by the same # Makefile don't overwrite each other's variables. PERLMOD_MAKEVAR_PREFIX = #--------------------------------------------------------------------------- # Configuration options related to the preprocessor #--------------------------------------------------------------------------- # If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will # evaluate all C-preprocessor directives found in the sources and include # files. ENABLE_PREPROCESSING = YES # If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro # names in the source code. If set to NO (the default) only conditional # compilation will be performed. Macro expansion can be done in a controlled # way by setting EXPAND_ONLY_PREDEF to YES. MACRO_EXPANSION = NO # If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES # then the macro expansion is limited to the macros specified with the # PREDEFINED and EXPAND_AS_DEFINED tags. EXPAND_ONLY_PREDEF = NO # If the SEARCH_INCLUDES tag is set to YES (the default) the includes files # in the INCLUDE_PATH (see below) will be search if a #include is found. SEARCH_INCLUDES = YES # The INCLUDE_PATH tag can be used to specify one or more directories that # contain include files that are not input files but should be processed by # the preprocessor. INCLUDE_PATH = # You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard # patterns (like *.h and *.hpp) to filter out the header-files in the # directories. If left blank, the patterns specified with FILE_PATTERNS will # be used. INCLUDE_FILE_PATTERNS = # The PREDEFINED tag can be used to specify one or more macro names that # are defined before the preprocessor is started (similar to the -D option of # gcc). The argument of the tag is a list of macros of the form: name # or name=definition (no spaces). If the definition and the = are # omitted =1 is assumed. To prevent a macro definition from being # undefined via #undef or recursively expanded use the := operator # instead of the = operator. PREDEFINED = # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then # this tag can be used to specify a list of macro names that should be expanded. # The macro definition that is found in the sources will be used. # Use the PREDEFINED tag if you want to use a different macro definition. EXPAND_AS_DEFINED = # If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then # doxygen's preprocessor will remove all function-like macros that are alone # on a line, have an all uppercase name, and do not end with a semicolon. Such # function macros are typically used for boiler-plate code, and will confuse # the parser if not removed. SKIP_FUNCTION_MACROS = YES #--------------------------------------------------------------------------- # Configuration::additions related to external references #--------------------------------------------------------------------------- # The TAGFILES option can be used to specify one or more tagfiles. # Optionally an initial location of the external documentation # can be added for each tagfile. The format of a tag file without # this location is as follows: # # TAGFILES = file1 file2 ... # Adding location for the tag files is done as follows: # # TAGFILES = file1=loc1 "file2 = loc2" ... # where "loc1" and "loc2" can be relative or absolute paths or # URLs. If a location is present for each tag, the installdox tool # does not have to be run to correct the links. # Note that each tag file must have a unique name # (where the name does NOT include the path) # If a tag file is not located in the directory in which doxygen # is run, you must also specify the path to the tagfile here. TAGFILES = # When a file name is specified after GENERATE_TAGFILE, doxygen will create # a tag file that is based on the input files it reads. GENERATE_TAGFILE = # If the ALLEXTERNALS tag is set to YES all external classes will be listed # in the class index. If set to NO only the inherited external classes # will be listed. ALLEXTERNALS = NO # If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed # in the modules index. If set to NO, only the current project's groups will # be listed. EXTERNAL_GROUPS = YES # The PERL_PATH should be the absolute path and name of the perl script # interpreter (i.e. the result of `which perl'). PERL_PATH = /usr/bin/perl #--------------------------------------------------------------------------- # Configuration options related to the dot tool #--------------------------------------------------------------------------- # If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will # generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base # or super classes. Setting the tag to NO turns the diagrams off. Note that # this option is superseded by the HAVE_DOT option below. This is only a # fallback. It is recommended to install and use dot, since it yields more # powerful graphs. CLASS_DIAGRAMS = NO # You can define message sequence charts within doxygen comments using the \msc # command. Doxygen will then run the mscgen tool (see # http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the # documentation. The MSCGEN_PATH tag allows you to specify the directory where # the mscgen tool resides. If left empty the tool is assumed to be found in the # default search path. MSCGEN_PATH = # If set to YES, the inheritance and collaboration graphs will hide # inheritance and usage relations if the target is undocumented # or is not a class. HIDE_UNDOC_RELATIONS = YES # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # available from the path. This tool is part of Graphviz, a graph visualization # toolkit from AT&T and Lucent Bell Labs. The other options in this section # have no effect if this option is set to NO (the default) HAVE_DOT = NO # By default doxygen will write a font called FreeSans.ttf to the output # directory and reference it in all dot files that doxygen generates. This # font does not include all possible unicode characters however, so when you need # these (or just want a differently looking font) you can specify the font name # using DOT_FONTNAME. You need need to make sure dot is able to find the font, # which can be done by putting it in a standard location or by setting the # DOTFONTPATH environment variable or by setting DOT_FONTPATH to the directory # containing the font. DOT_FONTNAME = FreeSans # The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs. # The default size is 10pt. DOT_FONTSIZE = 10 # By default doxygen will tell dot to use the output directory to look for the # FreeSans.ttf font (which doxygen will put there itself). If you specify a # different font using DOT_FONTNAME you can set the path where dot # can find it using this tag. DOT_FONTPATH = # If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen # will generate a graph for each documented class showing the direct and # indirect inheritance relations. Setting this tag to YES will force the # the CLASS_DIAGRAMS tag to NO. CLASS_GRAPH = YES # If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen # will generate a graph for each documented class showing the direct and # indirect implementation dependencies (inheritance, containment, and # class references variables) of the class with other documented classes. COLLABORATION_GRAPH = YES # If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen # will generate a graph for groups, showing the direct groups dependencies GROUP_GRAPHS = YES # If the UML_LOOK tag is set to YES doxygen will generate inheritance and # collaboration diagrams in a style similar to the OMG's Unified Modeling # Language. UML_LOOK = NO # If set to YES, the inheritance and collaboration graphs will show the # relations between templates and their instances. TEMPLATE_RELATIONS = YES # If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT # tags are set to YES then doxygen will generate a graph for each documented # file showing the direct and indirect include dependencies of the file with # other documented files. INCLUDE_GRAPH = YES # If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and # HAVE_DOT tags are set to YES then doxygen will generate a graph for each # documented header file showing the documented files that directly or # indirectly include this file. INCLUDED_BY_GRAPH = YES # If the CALL_GRAPH and HAVE_DOT options are set to YES then # doxygen will generate a call dependency graph for every global function # or class method. Note that enabling this option will significantly increase # the time of a run. So in most cases it will be better to enable call graphs # for selected functions only using the \callgraph command. CALL_GRAPH = NO # If the CALLER_GRAPH and HAVE_DOT tags are set to YES then # doxygen will generate a caller dependency graph for every global function # or class method. Note that enabling this option will significantly increase # the time of a run. So in most cases it will be better to enable caller # graphs for selected functions only using the \callergraph command. CALLER_GRAPH = NO # If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen # will graphical hierarchy of all classes instead of a textual one. GRAPHICAL_HIERARCHY = YES # If the DIRECTORY_GRAPH, SHOW_DIRECTORIES and HAVE_DOT tags are set to YES # then doxygen will show the dependencies a directory has on other directories # in a graphical way. The dependency relations are determined by the #include # relations between the files in the directories. DIRECTORY_GRAPH = YES # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # generated by dot. Possible values are png, jpg, or gif # If left blank png will be used. DOT_IMAGE_FORMAT = png # The tag DOT_PATH can be used to specify the path where the dot tool can be # found. If left blank, it is assumed the dot tool can be found in the path. DOT_PATH = # The DOTFILE_DIRS tag can be used to specify one or more directories that # contain dot files that are included in the documentation (see the # \dotfile command). DOTFILE_DIRS = # The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of # nodes that will be shown in the graph. If the number of nodes in a graph # becomes larger than this value, doxygen will truncate the graph, which is # visualized by representing a node as a red box. Note that doxygen if the # number of direct children of the root node in a graph is already larger than # DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note # that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. DOT_GRAPH_MAX_NODES = 50 # The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the # graphs generated by dot. A depth value of 3 means that only nodes reachable # from the root by following a path via at most 3 edges will be shown. Nodes # that lay further from the root node will be omitted. Note that setting this # option to 1 or 2 may greatly reduce the computation time needed for large # code bases. Also note that the size of a graph can be further restricted by # DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. MAX_DOT_GRAPH_DEPTH = 0 # Set the DOT_TRANSPARENT tag to YES to generate images with a transparent # background. This is disabled by default, because dot on Windows does not # seem to support this out of the box. Warning: Depending on the platform used, # enabling this option may lead to badly anti-aliased labels on the edges of # a graph (i.e. they become hard to read). DOT_TRANSPARENT = NO # Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output # files in one run (i.e. multiple -o and -T options on the command line). This # makes dot run faster, but since only newer versions of dot (>1.8.10) # support this, this feature is disabled by default. DOT_MULTI_TARGETS = NO # If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will # generate a legend page explaining the meaning of the various boxes and # arrows in the dot generated graphs. GENERATE_LEGEND = YES # If the DOT_CLEANUP tag is set to YES (the default) Doxygen will # remove the intermediate dot files that are used to generate # the various graphs. DOT_CLEANUP = YES asterisk-testsuite-0.0.0+svn.5781/lib/000077500000000000000000000000001242304700500173335ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/lib/lua/000077500000000000000000000000001242304700500201145ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/lib/lua/asttest.lua000066400000000000000000000020241242304700500223040ustar00rootroot00000000000000-- spawn the test driver and parse the results module(..., package.seeall) asttest = {} function asttest:new(test_dir) local a = { dir = test_dir, tests = {}, results = {}, totals = { ["pass"] = 0, ["fail"] = 0, ["xpass"] = 0, ["xfail"] = 0, ["skip"] = 0, ["error"] = 0, ["total"] = 0, }, } setmetatable(a, self) self.__index = self return a end function asttest:parse_output() for test, result in string.gmatch(self.output, "%d+%.%s+(%S+)%s+(%S+)\n") do table.insert(self.tests, test) self.results[test] = result self.totals[result] = self.totals[result] + 1 self.totals.total = self.totals.total + 1 end end function asttest:spawn() local output = io.popen("../../asttest 2>&1 " .. self.dir) if not output then return true, "error running asttest" end self.log = self.dir .. "/asttest.log" self.output = output:read("*a") self:parse_output() return false end function run(dir) local a = asttest:new(dir) e, reason = a:spawn() if e then return nil else return a end end asterisk-testsuite-0.0.0+svn.5781/lib/lua/cdr.lua000066400000000000000000000052301242304700500213670ustar00rootroot00000000000000-- load asterisk csv cdr file and parse it module(..., package.seeall) function new(a) return cdr:new(a) end cdr = {} cdr.__metatable = "cdr" function cdr:new(a) local c = { path = a:path("/var/log/asterisk/cdr-csv/Master.csv"), _records = {}, } setmetatable(c, self) local res, err = c:_parse() if not res then return res, err end return c end function cdr:len() return self:__len() end function cdr:__len() return #self._records end function cdr:__index(i) if type(i) == "number" then return rawget(self, "_records")[i] end return rawget(cdr, i) end function cdr:ipairs() return ipairs(self._records) end function cdr:records() local i = 1 local f = function(s) local r = s._records[i] i = i + 1 return r end return f, self end function cdr:_parse() local f, err = io.open(self.path) if not f then return f, err end local count = 1 for line in f:lines() do local r, err = cdr_record:new(line) if not r then return r, ("error parsing cdr on line %s (%s)"):format(count, err) end table.insert(self._records, r) count = count + 1 end return self end cdr_record = {} function cdr_record:new(line) local r = { index = { "accountcode", "src", "dst", "dcontext", "clid", "channel", "dstchannel", "lastapp", "lastdata", "start", "answer", "end", "duration", "billsec", "disposition", "amaflags", "uniqueid", "userfield", }, data = {}, } setmetatable(r, self) self._process_index(r) local res, err = self._parse(r, line) if not res then return res, err end return r end function cdr_record:_parse(line) -- this algorithm is adapted from Programming in Lua (1st Edition) -- chapter 20, section 4 line = line .. ',' -- ending comma local start = 1 repeat -- next field is quoted? (start with `"'?) if line:find('^"', start) then local a, c local i = start repeat -- find closing quote a, i, c = line:find('"("?),', i+1) until c ~= '"' -- quote not followed by quote? if not i then return nil, 'unmatched "' end local f = line:sub(start+1, i-2) table.insert(self.data, (f:gsub('""', '"'))) start = i+1 else -- unquoted; find next comma local nexti = line:find(',', start) table.insert(self.data, line:sub(start, nexti-1)) start = nexti + 1 end until start > line:len() return self.data end function cdr_record:_process_index() for k, v in ipairs(self.index) do self.index[v] = k end end function cdr_record:__index(i) if type(i) == "number" then return rawget(self, "data")[i] end return rawget(self, "data")[rawget(self, "index")[i]] end -- put cdr in the ast table too _G.ast.cdr = _G.cdr asterisk-testsuite-0.0.0+svn.5781/lib/lua/watcher.lua000066400000000000000000000104431242304700500222560ustar00rootroot00000000000000module(..., package.seeall) event = {} function event.new(event) local e = ast.manager.message:new() e["Event"] = event return e end etree = {} etree.__index = etree function etree:new(expect) local e = { expect = expect, c = nil, -- children multi = {}, received = nil, } setmetatable(e, self) return e end function etree:__tostring() return self:_print() end function etree:_print(prefix) local result = "" if not prefix then prefix = "" end if getmetatable(self.expect) == nil and type(self.expect) == 'table' then result = result .. prefix .. "{" for i, event in ipairs(self.expect) do if i == 1 then result = result .. event["Event"] else result = result .. ", " .. event["Event"] end end result = result .. "}\n" else result = result .. prefix .. self.expect["Event"] .. "\n" end if self.c then for i, etree in ipairs(self.c) do result = result .. etree:_print(prefix .. " ") end end return result end function etree:add(child) if getmetatable(child) ~= etree then child = etree:new(child) end if not self.c then self.c = {child} else table.insert(self.c, child) end return child end function etree:check_all(e) if getmetatable(self.expect) == nil and type(self.expect) == 'table' then local res = false for i, expect in ipairs(self.expect) do if not self.multi[i] and self:check(e, expect) then self.multi[i] = e res = true break end end self.received = self.multi for i, _ in ipairs(self.expect) do if not self.multi[i] then self.received = nil end end return res end if self:check(e, self.expect) then self.received = e return true end return false end function etree:check(e, expect) if type(expect) == 'function' then return expect(e) end if e["Event"] == expect["Event"] then local matched = true for i, h in ipairs(expect.headers) do local found = false for i, h2 in ipairs(e.headers) do if h[1] == h2[1] then if type(h[2]) == 'function' then local res = h[2](h2[2]) if res then found = true break end elseif h[2] == h2[2] then found = true break end end end if not found then matched = false break end end return matched end return false end function etree:check_next(e) if not self.received then return self:check_all(e) end if self.c then for i, etree in ipairs(self.c) do if etree:check_next(e) then return true end end end return false end function etree:matched() -- if this node has not been matched, return false if not self.received then return false end -- if this node has been matched and has no children, return true if not self.c then return true end -- check if any of the children have been matched if self.c then for i, e in ipairs(self.c) do if e:matched() then return true end end end -- none of the children matched return false end --- Watch the given manager connection for the given events within the given -- time period. -- @returns True on success and nil and an error message on failure. Currently -- the only error message is "timeout". function watch(m, tree, timeout) function tv2ms(tv) return tv.tv_sec * 1000 + tv.tv_usec / 1000 end if getmetatable(tree) == watcher.etree then tree = {tree} end function matched() for i, e in ipairs(tree) do if not e:matched() then return false end end return true end function handle_events(event) for i, e in ipairs(tree) do if e:check_next(event) then break end end end local start, err = posix.gettimeofday() if not start then return nil, err end start = tv2ms(start) m:register_event("", handle_events) while not matched() do local now, err = posix.gettimeofday() if not now then m:unregister_event("", handle_events) return nil, err end now = tv2ms(now) if timeout ~= 0 and timeout < now - start then m:unregister_event("", handle_events) return nil, "timeout" end local res, err = m:pump_messages() if not res then m:unregister_event("", handle_events) error("error processing events: " .. err) end m:process_events() if timeout == 0 then m:unregister_event("", handle_events) return nil, "timeout" end posix.usleep(1) end m:unregister_event("", handle_events) return tree end asterisk-testsuite-0.0.0+svn.5781/lib/python/000077500000000000000000000000001242304700500206545ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/lib/python/asterisk/000077500000000000000000000000001242304700500225015ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/lib/python/asterisk/__init__.py000066400000000000000000000000001242304700500246000ustar00rootroot00000000000000asterisk-testsuite-0.0.0+svn.5781/lib/python/asterisk/ami.py000066400000000000000000000714131242304700500236270ustar00rootroot00000000000000#!/usr/bin/env python # vim: sw=3 et: """ Copyright (C) 2010, Digium, Inc. This program is free software, distributed under the terms of the GNU General Public License Version 2. """ from twisted.internet import reactor from starpy import manager import datetime import logging import re import json from pluggable_registry import PLUGGABLE_EVENT_REGISTRY,\ PLUGGABLE_ACTION_REGISTRY, var_replace LOGGER = logging.getLogger(__name__) class AMIEventInstance(object): """Base class for specific instances of AMI event observers This handles common elements for both headermatch and callback types of AMI event observers, allowing the individual types to focus on their specific duties. """ def __init__(self, instance_config, test_object): """Constructor Keyword Arguments: instance_config The YAML configuration for the object test_object The main test object for the test """ self.test_object = test_object conditions = instance_config['conditions'] self.match_conditions = conditions['match'] self.nonmatch_conditions = conditions.get('nomatch', {}) self.ids = instance_config['id'].split(',') if 'id' in instance_config\ else ['0'] self.action = instance_config['action'] if 'action' in instance_config\ else 'none' self.config = instance_config self.passed = True self._registered = False self._event_observers = [] self.count = {} if 'count' in instance_config: count = instance_config['count'] if isinstance(count, int): # Need exactly this many events self.count['min'] = count self.count['max'] = count elif count[0] == '<': # Need at most this many events self.count['min'] = 0 self.count['max'] = int(count[1:]) elif count[0] == '>': # Need at least this many events self.count['min'] = int(count[1:]) self.count['max'] = float("inf") else: # Need exactly this many events self.count['min'] = int(count) self.count['max'] = int(count) else: self.count['min'] = 0 self.count['max'] = float("inf") self.count['event'] = 0 if 'type' in instance_config and instance_config['type'] == 'cel': # If the type is 'cel' and no condition matches are defined in the # test's yaml then create the dict with setting the Event to 'CEL'. # Otherwise set Event to 'CEL' since it's the only Event we want. if instance_config['conditions']['match'] is None: instance_config['conditions']['match'] = {'Event' : 'CEL'} self.match_conditions = instance_config['conditions']['match'] else: instance_config['conditions']['match']['Event'] = 'CEL' if 'Event' not in self.match_conditions: LOGGER.error("No event specified to match on. Aborting test") raise Exception test_object.register_ami_observer(self.ami_connect) test_object.register_stop_observer(self.__check_result) def ami_connect(self, ami): """AMI connect handler""" self.register_handler(ami) def register_handler(self, ami): """Register for the AMI events. Note: In general, most objects won't need this method. You would only call this from a derived object when you create instances of the derived object after AMI connect. """ if str(ami.id) in self.ids and not self._registered: LOGGER.debug("Registering event %s", self.match_conditions['Event']) ami.registerEvent(self.match_conditions['Event'], self.__event_callback) self._registered = True def register_event_observer(self, observer): """Register an observer to be called when a matched event is received An observer should take in two parameters: ami The AMI manager object event The received event """ self._event_observers.append(observer) def dispose(self, ami): """Dispose of this object's AMI event registrations""" if str(ami.id) not in self.ids: LOGGER.warning("Unable to dispose of AMIEventInstance - " \ "unknown AMI object %d", ami.id) return ami.deregisterEvent(self.match_conditions['Event'], self.__event_callback) def event_callback(self, ami, event): """Virtual method overridden by specific AMI Event instance types""" pass def __event_callback(self, ami, event): """Check event conditions to see if subclasses should be called into""" for key, value in self.match_conditions.items(): if key.lower() not in event: LOGGER.debug("Condition %s not in event, returning", key) return if not re.match(value, event.get(key.lower())): LOGGER.debug("Condition %s: %s does not match %s: %s in event", key, value, key, event.get(key.lower())) return else: LOGGER.debug("Condition %s: %s matches %s: %s in event", key, value, key, event.get(key.lower())) for key, value in self.nonmatch_conditions.items(): if key.lower() not in event: LOGGER.debug("Condition %s not in event, returning", key) return if re.match(value, event.get(key.lower())): LOGGER.debug("Condition %s: %s matches %s: %s in event", key, value, key, event.get(key.lower())) return else: LOGGER.debug("Condition %s: %s does not match %s: %s in event", key, value, key, event.get(key.lower())) self.count['event'] += 1 # Conditions have matched up as expected so leave it to the individual # types to determine how to proceed for observer in self._event_observers: observer(ami, event) # If this event instance has met the minimum number execute any # specified action. Note that if min is 0 this will never get reached, # so something else must terminate the test if self.count['event'] == self.count['min']: if self.action == 'stop': self.test_object.stop_reactor() return self.event_callback(ami, event) def check_result(self, callback_param): """Virtual method to be overridden by subclasses""" pass def __check_result(self, callback_param): """Verify results This will check against event counts and the like and then call into overridden versions via check_result """ if (self.count['event'] > self.count['max'] or self.count['event'] < self.count['min']): LOGGER.warning("Event occurred %d times, which is out of the" " allowable range", self.count['event']) LOGGER.warning("Event description: %s", str(self.config)) self.test_object.set_passed(False) return callback_param return self.check_result(callback_param) class AMIHeaderMatchInstance(AMIEventInstance): """A subclass of AMIEventInstance that operates by matching headers of AMI events to expected values. If a header does not match its expected value, then the test will fail """ def __init__(self, instance_config, test_object): """Constructor Keyword Arguments: instance_config The YAML configuration for the object test_object The main test object for the test """ super(AMIHeaderMatchInstance, self).__init__(instance_config, test_object) LOGGER.debug("Initializing an AMIHeaderMatchInstance") if 'requirements' in instance_config: self.match_requirements =\ instance_config['requirements'].get('match', {}) self.nonmatch_requirements =\ instance_config['requirements'].get('nomatch', {}) else: self.match_requirements = {} self.nonmatch_requirements = {} def event_callback(self, ami, event): """Callback called when an event is received from AMI""" for key, value in self.match_requirements.items(): if key.lower() not in event: LOGGER.warning("Requirement %s does not exist in event %s", key, event['event']) self.passed = False elif not re.match(value, event.get(key.lower())): LOGGER.warning("Requirement %s: %s does not match %s: %s in " \ "event", key, value, key, event.get(key.lower(), '')) self.passed = False else: LOGGER.debug("Requirement %s: %s matches %s: %s in event", key, value, key, event.get(key.lower())) for key, value in self.nonmatch_requirements.items(): if key.lower() not in event: LOGGER.warning("Requirement %s does not exist in event %s", key, event['event']) self.passed = False elif re.match(value, event.get(key.lower(), '')): LOGGER.warning("Requirement %s: %s matches %s: %s in event", key, value, key, event.get(key.lower(), '')) self.passed = False else: LOGGER.debug("Requirement %s: %s does not match %s: %s " \ "in event", key, value, key, event.get(key.lower(), '')) return (ami, event) def check_result(self, callback_param): """Deferred callback called when this object should verify pass/fail""" self.test_object.set_passed(self.passed) return callback_param class AMIOrderedHeaderMatchInstance(AMIEventInstance): """A subclass of AMIEventInstance that operates by matching headers of AMI events to expected values. If a header does not match its expected value, then the test will fail. This differs from AMIHeaderMatchInstance in that the order of specification is used to define an expected order for the events to arrive in which must be matched in order for the test to pass. """ def __init__(self, instance_config, test_object): """Constructor Keyword Arguments: instance_config The YAML configuration for the object test_object The main test object for the test """ super(AMIOrderedHeaderMatchInstance, self).__init__(instance_config, test_object) LOGGER.debug("Initializing an AMIOrderedHeaderMatchInstance") self.match_index = 0 self.match_requirements = [] self.nonmatch_requirements = [] for instance in instance_config['requirements']: self.match_requirements.append(instance.get('match', {})) self.nonmatch_requirements.append(instance.get('nomatch', {})) def event_callback(self, ami, event): """Callback called when an event is received from AMI""" if self.match_index >= len(self.match_requirements): LOGGER.debug("Event received and not defined: %s", event) return for key, value in self.match_requirements[self.match_index].items(): if key.lower() not in event: LOGGER.warning("Requirement %s does not exist in event %s", key, event['event']) self.passed = False elif not re.match(value, event.get(key.lower())): LOGGER.warning("Requirement %s: %s does not match %s: " \ "%s in event", key, value, key, event.get(key.lower())) self.passed = False else: LOGGER.debug("Requirement %s: %s matches %s: %s in event", key, value, key, event.get(key.lower())) for key, value in self.nonmatch_requirements[self.match_index].items(): if key.lower() not in event: LOGGER.warning("Requirement %s does not exist in event %s", key, event['event']) self.passed = False elif re.match(value, event.get(key.lower(), '')): LOGGER.warning("Requirement %s: %s matches %s: %s in event", key, value, key, event.get(key.lower(), '')) self.passed = False else: LOGGER.debug("Requirement %s: %s does not match %s: %s " "in event", key, value, key, event.get(key.lower(), '')) self.match_index += 1 return (ami, event) def check_result(self, callback_param): """Deferred callback called when this object should verify pass/fail""" self.test_object.set_passed(self.passed) return callback_param class CelRequirement(object): """A particular set of requirements that should be matched on for CEL event checking """ def __init__(self, requirements): """Constructor Keyword Arguments: requirements The CEL items to match on """ self.requirements = {} # Make everything case insensitive for sanity for key, value in requirements['match'].items(): lower_key = key.lower() if lower_key == 'extra': value = dict((key.lower(), value)\ for key, value in value.iteritems()) self.requirements[lower_key] = value self.orderings = requirements.get('partialorder') or [] self.named_id = requirements.get('id') def is_match(self, event): """Determine if this event matches us""" for key, value in event.items(): item = self.requirements.get(key) if item is None: continue # test 'Extra' fields against the JSON blob if key == "extra": if not len(value): continue extra_obj = json.loads(value) for extra_key, extra_value in extra_obj.items(): extra_item = item.get(extra_key.lower()) if extra_item is None: continue extra_match = re.match(extra_item, str(extra_value)) if extra_match is None or\ extra_match.end() != len(str(extra_value)): LOGGER.debug('Skipping %s - %s does not equal %s for ' 'extra-subfield %s', event['eventname'], extra_item, str(extra_value), extra_key) return False else: match = re.match(item, value) if match is None or match.end() != len(value): LOGGER.debug('Skipping %s - %s does not equal %s ' 'for field %s', event['eventname'], item, value, key) return False LOGGER.debug('Matched CEL event %s', event['eventname']) return True def __str__(self): return str(self.requirements) class AMICelInstance(AMIEventInstance): """A subclass of AMIEventInstance that operates by matching headers of AMI CEL events to expected values. This is similar to AMIOrderedHeaderMatchInstance but differs in that it's specifically for checking CEL events and that a partial order may be specified to allow some events to be out of order. """ # Class level list of all instances of this class ami_cel_instances = [] # All matched expected events that have an ordering matched_cel_events = [] # All unmatched expected events that have an ordering unmatched_cel_events = [] def __init__(self, instance_config, test_object): """Constructor Keyword Arguments: instance_config The YAML configuration for the object test_object The main test object for the test """ super(AMICelInstance, self).__init__(instance_config, test_object) self.match_requirements = [] self.test_object.register_stop_observer(self._stop_callback) # Creat our requirements for instance in instance_config['requirements']: self.match_requirements.append(CelRequirement(instance)) # Add of all our named events to the lists of events that haven't # occurred yet named_events = [ev for ev in self.match_requirements if\ ev.named_id is not None] AMICelInstance.unmatched_cel_events.extend(named_events) AMICelInstance.ami_cel_instances.append(self) def event_callback(self, ami, event): """Callback called by the base class when an event matches""" if len(self.match_requirements) == 0: return LOGGER.debug("Received CEL event %s", str(event)) req = self.match_requirements[0] if not req.is_match(event): LOGGER.debug("Dropping event %s - next required event is %s", event['eventname'], req.requirements['eventname']) return self.match_requirements.pop(0) if len(req.orderings) > 0: self._check_orderings(req) if req.named_id is not None: AMICelInstance.unmatched_cel_events.remove(req) AMICelInstance.matched_cel_events.append(req) def _stop_callback(self, reason): """Stop observer on the test_object. Called when Asterisk has stopped at the end of the test""" if len(self.match_requirements) != 0: LOGGER.warning("Length of expected CEL requirements not zero: %d", len(self.match_requirements)) LOGGER.warning("Missed CEL requirement: %s", str(self.match_requirements[0])) self.test_object.set_passed(False) return reason LOGGER.info("All expected CEL requirements matched") self.test_object.set_passed(True) return reason def _check_orderings(self, cel_requirement): """Check that this matched CelRequirement occurred in the right order""" for order_type, named_event in cel_requirement.orderings.items(): order_type = order_type.lower() if order_type == 'after': matches = [ev for ev in AMICelInstance.matched_cel_events if\ ev.named_id == named_event] if len(matches) == 0: LOGGER.warning('Event %s did not occur after %s; failing', str(cel_requirement), named_event) self.test_object.set_passed(False) elif order_type == 'before': matches = [ev for ev in AMICelInstance.unmatched_cel_events if\ ev.named_id == named_event] if len(matches) == 0: LOGGER.warning('Event %s did not occur before %s; failing', str(cel_requirement), named_event) self.test_object.set_passed(False) else: LOGGER.warning('Unknown partialorder type %s; ignoring', order_type) class AMICallbackInstance(AMIEventInstance): """Subclass of AMIEventInstance that operates by calling a user-defined callback function. The callback function returns the current disposition of the test (i.e. whether the test is currently passing or failing). """ def __init__(self, instance_config, test_object): """Constructor Keyword Arguments: instance_config The YAML configuration for the object test_object The main test object for the test """ super(AMICallbackInstance, self).__init__(instance_config, test_object) self.callback_module = instance_config['callbackModule'] self.callback_method = instance_config['callbackMethod'] if 'start' in instance_config: self.passed = True if instance_config['start'] == 'pass' else False def event_callback(self, ami, event): """Callback called when an event is received from AMI""" callback_module = __import__(self.callback_module) method = getattr(callback_module, self.callback_method) self.passed = method(ami, event) if self.passed == None: LOGGER.error("Callback %s.%s returned None instead of a boolean", self.callback_module, self.callback_method) self.passed = False def check_result(self, callback_param): """Deferred callback called when this object should verify pass/fail""" self.test_object.set_passed(self.passed) return callback_param class AMIEventInstanceFactory: """Factory object that builds concrete instances of various AMIEventModules. Supported types: headermach - Construct a AMIHeaderMatchInstance object orderedheadermatch - Construct a AMIOrderedHeadermatchInstance object cel - Construct a AMICelInstance object callback - Construct a AMICallbackInstance object """ @staticmethod def create_instance(instance_config, test_object): """Create an AMI event matching instance object Keyword Arguments: instance_config The instance object's configuration test_object The pluggable module framework's test object for the test """ instance_type = instance_config['type'] if instance_type == "headermatch": LOGGER.debug("instance type is 'headermatch'") return AMIHeaderMatchInstance(instance_config, test_object) elif instance_type == "orderedheadermatch": LOGGER.debug("instance type is 'orderedheadermatch'") return AMIOrderedHeaderMatchInstance(instance_config, test_object) elif instance_type == "cel": LOGGER.debug("instance type is 'cel'") return AMICelInstance(instance_config, test_object) elif instance_type == "callback": LOGGER.debug("instance type is 'callback'") return AMICallbackInstance(instance_config, test_object) else: LOGGER.error("Invalid type %s specified for AMI event instance", instance_type) raise Exception class AMIEventModule(object): """Pluggable module for AMI event matching This class acts as a very thin manager for multiple instances of objects derived from AMIEventInstance. It merely exists to create the objects and plug into the Pluggable Module Framework. """ def __init__(self, module_config, test_object): """Constructor Keyword Arguments: instance_config The YAML configuration for the object test_object The main test object for the test """ LOGGER.debug("Initializing AMIEvent module") self.test_object = test_object self.ami_instances = [] for instance in module_config: event_instance = AMIEventInstanceFactory\ .create_instance(instance, test_object) self.ami_instances.append(event_instance) class AMI(object): """Class that manages a connection to Asterisk over AMI""" def __init__(self, on_login, on_error, timeout=60, user="mark", secret="mysecret", host="127.0.0.1", port=5038): """Constructor Keyword Arguments: on_login Deferred callback to be called when user logs into AMI on_error Deferred callback to be called if an error occurs while logging into AMI timeout Time to wait before failing login attempts user User to connect as secret Password for the user host Asterisk instance to connect to port Port to connect on """ self.on_login = on_login self.on_error = on_error self.login_timeout = timeout self.host = host self.port = port self._attempts = 0 self._start = None self.ami = None self.ami_factory = manager.AMIFactory(user, secret) def login(self): """Start the login process""" self._attempts += 1 LOGGER.debug("AMI Login attempt #%d", self._attempts) if not self._start: self._start = datetime.datetime.now() deferred = self.ami_factory.login(self.host, self.port) deferred.addCallbacks(self.on_login_success, self.on_login_error) def on_login_success(self, ami): """Deferred callback when login succeeds Keyword Arguments: ami The AMI Factory object """ self.ami = ami LOGGER.debug("AMI Login succesful") return self.on_login(ami) def on_login_error(self, reason): """Deferred callback when login fails This will continue to attempt logging in until self.timeout is reached. If the timeout is reached, the login error deferred passed to the constructor is called. Keyword Arguments: reason The reason why the login attempt failed. """ runtime = (datetime.datetime.now() - self._start).seconds if runtime >= self.login_timeout: LOGGER.error("AMI login failed after %d second timeout: %s", self.login_timeout, reason.getErrorMessage()) return self.on_error() delay = 2 ** self._attempts if delay + runtime >= self.login_timeout: delay = self.login_timeout - runtime reactor.callLater(delay, self.login) return reason class AMIStartEventModule(object): """An event module that triggers when the test starts.""" def __init__(self, test_object, triggered_callback, config): """Setup the test start observer""" self.test_object = test_object self.triggered_callback = triggered_callback self.config = config test_object.register_ami_observer(self.start_observer) def start_observer(self, ami): """Notify the event-action mapper that ami has started.""" self.triggered_callback(self, ami) PLUGGABLE_EVENT_REGISTRY.register("ami-start", AMIStartEventModule) class AMIPluggableEventInstance(AMIHeaderMatchInstance): """Subclass of AMIEventInstance that works with the pluggable event action module. """ def __init__(self, test_object, triggered_callback, config, data): """Setup the AMI event observer""" self.triggered_callback = triggered_callback self.data = data super(AMIPluggableEventInstance, self).__init__(config, test_object) def event_callback(self, ami, event): """Callback called when an event is received from AMI""" super(AMIPluggableEventInstance, self).event_callback(ami, event) if self.passed: self.triggered_callback(self.data, ami, event) class AMIPluggableEventModule(object): """Generates AMIEventInstance instances that match events for the pluggable event-action framework. """ def __init__(self, test_object, triggered_callback, config): """Setup the AMI event observers""" self.instances = [] if not isinstance(config, list): config = [config] for instance in config: self.instances.append(AMIPluggableEventInstance(test_object, triggered_callback, instance, self)) PLUGGABLE_EVENT_REGISTRY.register("ami-events", AMIPluggableEventModule) def replace_ami_vars(mydict, values): outdict = {} for key, value in mydict.iteritems(): outdict[key] = var_replace(value, values) return outdict class AMIPluggableActionModule(object): """Pluggable AMI action module. """ def __init__(self, test_object, config): """Setup the AMI event observer""" self.test_object = test_object if not isinstance(config, list): config = [config] self.config = config def run(self, triggered_by, source, extra): """Callback called when this action is triggered.""" for instance in self.config: action = replace_ami_vars(instance["action"], extra) ami_id = instance.get("id", 0) self.test_object.ami[ami_id].sendMessage(action) PLUGGABLE_ACTION_REGISTRY.register("ami-actions", AMIPluggableActionModule) asterisk-testsuite-0.0.0+svn.5781/lib/python/asterisk/apptest.py000066400000000000000000001004721242304700500245370ustar00rootroot00000000000000"""Application test module for the pluggable module framework This pluggable test-object and modules allows a test configuration to control a Local channel in a long running Asterisk application. This is suitable for testing Asterisk applications such as VoiceMail, ConfBridge, MeetMe, etc. Copyright (C) 2012, Digium, Inc. Matt Jordan This program is free software, distributed under the terms of the GNU General Public License Version 2. """ import sys import logging import uuid from twisted.internet import reactor, defer sys.path.append("lib/python") from test_case import TestCase from ami import AMIEventInstance LOGGER = logging.getLogger(__name__) class AppTest(TestCase): """A pluggable test object suitable for orchestrating tests against long running Asterisk applications""" _singleton_instance = None @staticmethod def get_instance(path='', test_config=None): """Return the singleton instance of the application test_object Keyword Arguments: path The full path to the location of the test test_config The test's YAML configuration object """ if (AppTest._singleton_instance is None): # Note that the constructor sets the singleton instance. # This is a tad backwards, but is needed for the pluggable # framework. AppTest(path, test_config) return AppTest._singleton_instance def __init__(self, path, test_config): """Create the pluggable test module Keyword Arguments: path - The full path to the location of the test test_config - This test's configuration """ super(AppTest, self).__init__(path) self._channel_objects = {} # The current scenario's channels self._expected_results = {} # Expected results for all scenarios self._event_instances = [] # The ApplicationEventInstance objects self.raw_test_config = test_config if 'app' in self.raw_test_config: self._applications = [ self.raw_test_config['app'] ] elif 'apps' in self.raw_test_config: self._applications = self.raw_test_config['apps'] else: raise Exception("No 'app' or 'apps' defined in test-config") self._scenarios = self.raw_test_config['scenarios'] self.register_ami_observer(self._ami_connect_handler) self.register_stop_observer(self.end_scenario) self.create_asterisk() # Created successfully - set the singleton instance to this object # if we're the first instance created; otherwise, complain loudly if (AppTest._singleton_instance is None): AppTest._singleton_instance = self else: raise Exception("Singleton instance of AppTest already set!") def run(self): """Run the test. Called when the reactor is started.""" super(AppTest, self).run() self.create_ami_factory() def _run_scenario(self, scenario): """Run some scenario Keyword Arguments: scenario The scenario object to execute """ LOGGER.info("Starting scenario...") # Create event instances not associated with a channel if 'events' in scenario: for event_config in scenario['events']: ae_instance = ApplicationEventInstance('', event_config, self) self._event_instances.append(ae_instance) # Create the event instances associated with a channel and the # corresponding channel object for channel_config in scenario['channels']: channel_id = channel_config['channel-id'] for event_config in channel_config['events']: ae_instance = ApplicationEventInstance(channel_id, event_config, self) self._event_instances.append(ae_instance) obj = ChannelObject(ami=self.ami[0], applications=self._applications, channel_def=channel_config) self._channel_objects[channel_id] = obj LOGGER.debug("Created channel object for %s" % channel_id) def _ami_connect_handler(self, ami): """Handler for the AMI connect event Starts the first scenario object """ self._run_scenario(self._scenarios.pop(0)) return ami def _reset_scenario_objects(self): """Reset the scenario objects for the next iteration""" self._channel_objects.clear() self._expected_results.clear() for event_instance in self._event_instances: event_instance.dispose(self.ami[0]) self._event_instances = [] def _evaluate_expected_results(self): """Evaluate expected results for a scenario""" if (len(self._expected_results) == 0): self.set_passed(True) return for expected, result in self._expected_results.items(): if not result: LOGGER.warn("Expected result %s failed!" % expected) self.set_passed(False) else: LOGGER.debug("Expected result %s passed" % expected) self.set_passed(True) def end_scenario(self, result=None): """End the current scenario""" self._evaluate_expected_results() if len(self._scenarios) == 0: LOGGER.info("All scenarios executed; stopping") self.stop_reactor() else: self._reset_scenario_objects() self.reset_timeout() self._run_scenario(self._scenarios.pop(0)) return result def get_channel_object(self, channel_id): """Get the ChannelObject associated with a channel name Keywords: channel_id The ID of the channel to retrieve """ if channel_id not in self._channel_objects: LOGGER.error("Unknown channel %s requested from Scenario" % channel_id) raise Exception return self._channel_objects[channel_id] def add_expected_result(self, expected_result): """Add an expected result to the test_object Keywords: expected_result The name of the result that should occur """ self._expected_results[expected_result] = False def set_expected_result(self, expected_result): """Set an expected result to True Keywords: expected_result The name of the result that occurred """ self._expected_results[expected_result] = True self.reset_timeout() class ChannelObject(object): """Object that represents a channel in an application and its controlling mechanism. All tests use Local channels. One end of the Local channel pair is sent into the application. The other is dropped into a set of extensions that determine how the application is manipulated. AMI redirects are used to manipulate the second half of the Local channel pair. """ default_context = 'default' default_dtmf_exten = 'sendDTMF' default_hangup_exten = 'hangup' default_wait_exten = 'wait' default_audio_exten = 'sendAudio' def __init__(self, ami, applications, channel_def): ''' Create a new ChannelObject Keywords: ami The AMI instance to spawn the channel in applications The application names to test channel_def A dictionary of parameters to extract that will configure the channel object ''' self._channel_id = channel_def['channel-id'] self._channel_name = channel_def['channel-name'] self._applications = applications self._controller_context = channel_def.get('context') or \ ChannelObject.default_context self._controller_initial_exten = channel_def.get('exten') or \ ChannelObject.default_wait_exten self._controller_hangup_exten = channel_def.get('hangup-exten') or \ ChannelObject.default_hangup_exten self._controller_audio_exten = channel_def.get('audio-exten') or \ ChannelObject.default_audio_exten self._controller_dtmf_exten = channel_def.get('dtmf-exten') or \ ChannelObject.default_dtmf_exten self._controller_wait_exten = channel_def.get('wait-exten') or \ ChannelObject.default_wait_exten delay = channel_def.get('delay') or 0 self.ami = ami self.ami.registerEvent('Hangup', self._hangup_event_handler) self.ami.registerEvent('VarSet', self._varset_event_handler) self.ami.registerEvent('TestEvent', self._test_event_handler) self.ami.registerEvent('Newexten', self._new_exten_handler) self.ami.registerEvent('Newchannel', self._new_channel_handler) self._all_channels = [] # All channels we've detected self._candidate_channels = [] # The local pair that are ours self.app_channel = '' # The local half in the application self.controller_channel = '' # The local half controlling the test self._hungup = False self._previous_dtmf = '' self._previous_sound_file = '' self._test_observers = [] self._hangup_observers = [] self._candidate_prefix = '' self._unique_id = str(uuid.uuid1()) if 'start-on-create' in channel_def and channel_def['start-on-create']: self.spawn_call(delay) def spawn_call(self, delay=0): """Spawn the call! Keyword Arguments: delay The amount of time to wait before spawning the call Returns: Deferred object that will be called after the call has been originated. The deferred will pass this object as the parameter. """ def __spawn_call_callback(spawn_call_deferred): """Actually perform the origination""" self.ami.originate(channel=self._channel_name, context=self._controller_context, exten=self._controller_initial_exten, priority='1', variable={'testuniqueid': '%s' % self._unique_id}) spawn_call_deferred.callback(self) spawn_call_deferred = defer.Deferred() reactor.callLater(delay, __spawn_call_callback, spawn_call_deferred) return spawn_call_deferred def __str__(self): return '(Controller: %s; Application %s)' % (self.controller_channel, self.app_channel) def _handle_redirect_failure(self, reason): """If a redirect fails, complain loudly""" LOGGER.warn("Error occurred while sending redirect:") LOGGER.warn(reason.getTraceback()) return reason def _send_redirect(self, extension): """Redirect the controlling channel into some extension""" if self._hungup: LOGGER.debug("Ignoring redirect to %s; channel %s is hungup" % (extension, self.controller_channel)) return deferred = self.ami.redirect(self.controller_channel, self._controller_context, extension, 1) deferred.addErrback(self._handle_redirect_failure) def hangup(self, delay=0): """Hang up the channel Keywords: delay How long to wait before hanging up the channel Returns: A deferred object called when the hangup is initiated """ def __hangup_callback(hangup_deferred): """Deferred callback when a hangup has started""" self._send_redirect(self._controller_hangup_exten) hangup_deferred.callback(self) hangup_deferred = defer.Deferred() reactor.callLater(delay, __hangup_callback, hangup_deferred) return hangup_deferred def is_hungup(self): """Return whether or not the channels owned by this object are hungup""" return self._hungup def register_test_observer(self, callback): """Register an observer to be called when a test event is fired that affects this channel The callback called will be passed two parameters: 1) This object 2) The test event that caused the callback to be called """ self._test_observers.append(callback) def register_hangup_observer(self, callback): """Register an observer to be called when a hangup is detected The callback called will be passed two parameters: 1) This object 2) The hangup event that caused the callback to be called """ self._hangup_observers.append(callback) def send_dtmf(self, dtmf, delay=0): """Send DTMF into the conference Keywords: dtmf The DTMF string to send delay Schedule the sending of the DTMF for some time period Returns: A deferred object that will be called when the DTMF starts to be sent. The callback parameter will be this object. """ def __send_dtmf_initial(param): """Initial callback called by the reactor. This sets the dialplan variable DTMF_TO_SEND to the dtmf value to stream""" dtmf, dtmf_deferred = param if (self._previous_dtmf != dtmf): deferred = self.ami.setVar(channel=self.controller_channel, variable='DTMF_TO_SEND', value=dtmf) deferred.addCallback(__send_dtmf_redirect, dtmf_deferred) self._previous_dtmf = dtmf else: __send_dtmf_redirect(None, dtmf_deferred) def __send_dtmf_redirect(result, deferred): """Second callback called when the dialplan variable has been set. This redirect the controlling channel to the sendDTMF extension""" self._send_redirect(self._controller_dtmf_exten) deferred.callback(self) return deferred LOGGER.debug("Sending DTMF %s over Controlling Channel %s" % (dtmf, self.controller_channel)) dtmf_deferred = defer.Deferred() reactor.callLater(delay, __send_dtmf_initial, (dtmf, dtmf_deferred)) return dtmf_deferred def stream_audio(self, sound_file, delay=0): """Stream an audio sound file into the conference Keywords: sound_file The path of the sound file to stream delay Schedule the sending of the audio for some time period Returns: A deferred object that will be called when the aduio starts to be sent. The callback parameter will be this object. """ def __stream_audio_initial(param): """Initial callback called by the reactor. This sets the dialplan variable TALK_AUDIO to the file to stream""" sound_file, audio_deferred = param if (self._previous_sound_file != sound_file): deferred = self.ami.setVar(channel=self.controller_channel, variable="TALK_AUDIO", value=sound_file) deferred.addCallback(__stream_audio_redirect, audio_deferred) self._previous_sound_file = sound_file else: __stream_audio_redirect(None, audio_deferred) def __stream_audio_redirect(result, deferred): """Second callback called when the dialplan variable has been set. This redirect the controlling channel to the sendAudio extension""" self._send_redirect(self._controller_audio_exten) deferred.callback(self) return deferred LOGGER.debug("Streaming Audio File %s over Controlling Channel %s" % (sound_file, self.controller_channel)) audio_deferred = defer.Deferred() reactor.callLater(delay, __stream_audio_initial, (sound_file, audio_deferred)) return audio_deferred def stream_audio_with_dtmf(self, sound_file, dtmf, sound_delay=0, dtmf_delay=0): """Stream an audio sound file into the conference followed by some DTMF Keywords: sound_file The path of the sound file to stream dtmf The DTMF to send sound_delay Schedule the sending of the audio for some time period dtmf_delay Schedule the sending of the DTMF for some time period Returns: A deferred object that will be called when both the audio and dtmf have been triggered """ def __start_dtmf(param): """Triggered when the audio has started""" dtmf, dtmf_delay, audio_dtmf_deferred = param start_deferred = self.send_dtmf(dtmf, dtmf_delay) start_deferred.addCallback(__dtmf_sent, audio_dtmf_deferred) return param def __dtmf_sent(result, deferred): """Triggered when the DTMF has started""" deferred.callback(self) return deferred audio_dtmf_deferred = defer.Deferred() param_tuple = (dtmf, dtmf_delay, audio_dtmf_deferred) deferred = self.stream_audio(sound_file, sound_delay) deferred.addCallback(__start_dtmf, param_tuple) return audio_dtmf_deferred def _evaluate_candidates(self): """Determine if we know who our candidate channel is""" if len(self._candidate_prefix) == 0: return for channel in self._all_channels: if self._candidate_prefix in channel: LOGGER.debug("Adding candidate channel %s" % channel) self._candidate_channels.append(channel) def _new_channel_handler(self, ami, event): """Handler for the Newchannel event""" if event['channel'] not in self._all_channels: self._all_channels.append(event['channel']) self._evaluate_candidates() return (ami, event) def _hangup_event_handler(self, ami, event): """Handler for the Hangup event""" if self._hungup: # Don't process multiple hangup events return if 'channel' not in event: return if self.controller_channel == event['channel']: LOGGER.debug("Controlling Channel %s hangup event detected" % self.controller_channel) elif self.app_channel == event['channel']: LOGGER.debug("Application Channel %s hangup event detected" % self.app_channel) else: # Not us! return for observer in self._hangup_observers: observer(self, event) self._hungup = True return (ami, event) def _varset_event_handler(self, ami, event): """Handler for the VarSet event Note that we only care about the testuniqueid channel variable, which will tell us which channels we're responsible for """ if (event['variable'] != 'testuniqueid'): return if (event['value'] != self._unique_id): return channel_name = event['channel'][:len(event['channel'])-2] LOGGER.debug("Detected channel %s" % channel_name) self._candidate_prefix = channel_name self._evaluate_candidates() return (ami, event) def _test_event_handler(self, ami, event): """Handler for test events""" if 'channel' not in event: return if self.app_channel not in event['channel'] and \ self.controller_channel not in event['channel']: return for observer in self._test_observers: observer(self, event) return (ami, event) def _new_exten_handler(self, ami, event): """Handler for new extensions. This figures out which half of a local channel dropped into the specified app""" if 'channel' not in event or 'application' not in event: return if event['application'] not in self._applications: return if event['channel'] not in self._candidate_channels: # Whatever channel just entered isn't one of our channels. This # could occur if multiple channels are entering a Conference in a # test. return self.app_channel = event['channel'] self._candidate_channels.remove(event['channel']) if (';2' in self.app_channel): controller_name = self.app_channel.replace(';2', '') + ';1' else: controller_name = self.app_channel.replace(';1', '') + ';2' self.controller_channel = controller_name self._candidate_channels.remove(controller_name) LOGGER.debug("Setting App Channel to %s; Controlling Channel to %s" % (self.app_channel, self.controller_channel)) return (ami, event) class ApplicationEventInstance(AMIEventInstance): """An object that responds to AMI events that occur while a channel is in an application and initiates a sequence of actions on a channel object as a result Note that this is a pluggable object, but is created automatically by the configuration of the AppTest test object. """ def __init__(self, channel_id, instance_config, test_object): """Constructor Keyword Arguments: channel_id The unique ID of the channel pair instance_config The configuration object for this pluggable object test_object The test object this pluggable instance attaches to """ super(ApplicationEventInstance, self).__init__(instance_config, test_object) self.channel_id = channel_id self.actions = [] # create actions from the definitions for action_def in instance_config['actions']: action = ActionFactory.create_action(action_def) LOGGER.debug('Adding action %s, matching on %s' % ( action, self.match_conditions)) self.actions.append(action) self.__current_action = 0 self.channel_obj = None self.test_object = test_object # Force registration, as this object may be used in a scenario that # is executed long after AMI connection self.register_handler(self.test_object.ami[0]) def event_callback(self, ami, event): """Override of AMIEventInstance event_callback.""" # If we aren't matching on a channel, then just execute the actions if 'channel' not in event or len(self.channel_id) == 0: self.execute_next_action(actions=self.actions) return self.channel_obj = self.test_object.get_channel_object(self.channel_id) # Its possible that the event matching could only be so accurate, as # there may be multiple Local channel in the same extension. Make # sure that this is actually for us by checking the Asterisk channel # names if (self.channel_obj.app_channel in event['channel'] or self.channel_obj.controller_channel in event['channel']): self.execute_next_action(actions=self.actions) def execute_next_action(self, result=None, actions=None): """Execute the next action in the sequence""" if (not actions or len(actions) == 0): self.__current_action = 0 return LOGGER.debug("Executing action %d on %s" % (self.__current_action, str(self.channel_obj))) action = actions.pop(0) ret_obj = action(self.channel_obj) self.__current_action += 1 if ret_obj is not None: ret_obj.addCallback(self.execute_next_action, actions=actions) else: reactor.callLater(0, self.execute_next_action, actions=actions) return result def dispose(self, ami): """Have this object remove itself from the AMI connection""" super(ApplicationEventInstance, self).dispose(ami) # Clear the actions just to ensure they can't be executed again self.actions = [] class ActionStartCall(object): """Functor that spawns a call""" def __init__(self, action_config): """Constructor Keyword Arguments: action_config The config dictionary for this functor """ self.test_object = AppTest.get_instance() self.channel_id = action_config['channel-id'] self.delay = 0 if 'delay' not in action_config \ else action_config['delay'] def __call__(self, channel_object): spawn_channel = self.test_object.get_channel_object(self.channel_id) return spawn_channel.spawn_call(delay=self.delay) class ActionSendDtmf(object): """Functor that sends DTMF to a channel""" def __init__(self, action_config): """Constructor Keyword Arguments: action_config The config dictionary for this functor """ self.dtmf = action_config['dtmf'] self.delay = 0 if 'delay' not in action_config \ else int(action_config['delay']) if 'channel-id' in action_config: self.channel_id = action_config['channel-id'] else: self.channel_id = '' def __call__(self, channel_object): channel = channel_object if (len(self.channel_id) > 0): test_object = AppTest.get_instance() channel = test_object.get_channel_object(self.channel_id) LOGGER.debug("Sending DTMF %s over Controlling Channel %s" % (self.dtmf, channel)) return channel.send_dtmf(dtmf=self.dtmf, delay=self.delay) class ActionStreamAudio(object): """Functor that streams audio to a channel""" def __init__(self, action_config): """Constructor Keyword Arguments: action_config The config dictionary for this functor """ self.sound_file = action_config['sound-file'] self.delay = 0 if 'delay' not in action_config \ else int(action_config['delay']) def __call__(self, channel_object): return channel_object.stream_audio(sound_file=self.sound_file, delay=self.delay) class ActionStreamAudioWithDtmf(object): """Functor that streams audio followed by dtmf to a channel""" def __init__(self, action_config): """Constructor Keyword Arguments: action_config The config dictionary for this functor """ self.sound_file = action_config['sound-file'] self.dtmf = action_config['dtmf'] self.dtmf_delay = 0 if 'dtmf-delay' not in action_config \ else int(action_config['dtmf-delay']) self.sound_delay = 0 if 'sound-delay' not in action_config \ else int(action_config['sound-delay']) def __call__(self, channel_object): return channel_object.stream_audio_with_dtmf(sound_file=self.sound_file, dtmf=self.dtmf, sound_delay=self.sound_delay, dtmf_delay=self.dtmf_delay) class ActionSetExpectedResult(object): """Functor that sets some expected result on the channel object""" def __init__(self, action_config): """Constructor Keyword Arguments: action_config The config dictionary for this functor """ self.expected_result = action_config['expected-result'] self.test_object = AppTest.get_instance() self.test_object.add_expected_result(self.expected_result) def __call__(self, channel_object): def __raise_deferred(param): """Raise the deferred callback notifying everyone of the result""" deferred, channel_object = param deferred.callback(channel_object) return param LOGGER.info("Expected Result: %s" % self.expected_result) self.test_object.set_expected_result(self.expected_result) deferred_result = defer.Deferred() param = (deferred_result, channel_object) reactor.callLater(0, __raise_deferred, param) return deferred_result class ActionHangup(object): """Functor that hangs the channel up""" def __init__(self, action_config): self.delay = 0 if 'delay' not in action_config \ else int(action_config['delay']) if 'channel-id' in action_config: self.channel_id = action_config['channel-id'] else: self.channel_id = '' def __call__(self, channel_object): hangup_channel = channel_object if (len(self.channel_id) > 0): test_object = AppTest.get_instance() hangup_channel = test_object.get_channel_object(self.channel_id) LOGGER.info("Hanging up channel object %s" % str(hangup_channel)) return hangup_channel.hangup(self.delay) class ActionFailTest(object): """Functor that auto-fails the test""" def __init__(self, action_config): self.message = "Auto failing test!" if 'message' not in action_config \ else action_config['message'] def __call__(self, channel_object): test_object = AppTest.get_instance() LOGGER.error(self.message) test_object.set_passed(False) return None class ActionEndScenario(object): """Functor that signals to the AppTest object that the scenario has ended""" def __init__(self, action_config): self.message = "Ending scenario..." if 'message' not in action_config \ else action_config['message'] def __call__(self, channel_object): test_object = AppTest.get_instance() LOGGER.info(self.message) test_object.end_scenario() return None class ActionSendMessage(object): """Functor that sends some AMI message""" def __init__(self, action_config): self.add_app_channel = action_config.get('add-app-channel') or False self.add_control_channel = action_config.get('add-control-channel') or False self.channel_id = action_config.get('channel-id') or None if ((self.add_app_channel and self.add_control_channel) or (self.add_app_channel and self.channel_id) or (self.add_control_channel and self.channel_id)): raise Exception('Only one channel can be added to the message!') self.message_fields = action_config['fields'] def __call__(self, channel_object): if self.add_app_channel: self.message_fields['Channel'] = channel_object.app_channel elif self.add_control_channel: self.message_fields['Channel'] = channel_object.controller_channel elif self.channel_id: test_object = AppTest.get_instance() self.message_fields['Channel'] = test_object.get_channel_object(self.channel_id).app_channel LOGGER.debug('Sending message: %s' % str(self.message_fields)) channel_object.ami.sendMessage(self.message_fields) return None class ActionFactory(object): """A static class factory that maps action objects to text descriptions of those objects, and provides a factory method for creating them""" __action_definitions = {'start-call': ActionStartCall, 'send-dtmf': ActionSendDtmf, 'stream-audio': ActionStreamAudio, 'stream-audio-with-dtmf': ActionStreamAudioWithDtmf, 'set-expected-result': ActionSetExpectedResult, 'hangup': ActionHangup, 'fail-test': ActionFailTest, 'end-scenario': ActionEndScenario, 'send-ami-message': ActionSendMessage,} @staticmethod def create_action(action_def): """Create the specified action Returns: An action functor that must be called with the channel to invoke the action on """ action_type = action_def['action-type'] if action_type not in ActionFactory.__action_definitions: raise ValueError('Unknown Action Type %s' % action_type) return ActionFactory.__action_definitions[action_type](action_def) asterisk-testsuite-0.0.0+svn.5781/lib/python/asterisk/ari.py000066400000000000000000000631171242304700500236360ustar00rootroot00000000000000""" Copyright (C) 2013, Digium, Inc. David M. Lee, II This program is free software, distributed under the terms of the GNU General Public License Version 2. """ import datetime import json import logging import re import requests import traceback import urllib from test_case import TestCase from pluggable_registry import PLUGGABLE_EVENT_REGISTRY,\ PLUGGABLE_ACTION_REGISTRY, var_replace from test_suite_utils import all_match from twisted.internet import reactor try: from autobahn.websocket import WebSocketClientFactory, \ WebSocketClientProtocol, connectWS except: from autobahn.twisted.websocket import WebSocketClientFactory, \ WebSocketClientProtocol, connectWS LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 8088 #: Default matcher to ensure we don't have any validation failures on the # WebSocket VALIDATION_MATCHER = { 'conditions': { 'match': { 'error': "^InvalidMessage$" } }, 'count': 0 } class AriBaseTestObject(TestCase): """Class that acts as a Test Object in the pluggable module framework""" def __init__(self, test_path='', test_config=None): """Constructor for a test object :param test_path The full path to the test location :param test_config The YAML test configuration """ super(AriBaseTestObject, self).__init__(test_path, test_config) if test_config is None: # Meh, just use defaults test_config = {} self.apps = test_config.get('apps', 'testsuite') if isinstance(self.apps, list): self.apps = ','.join(self.apps) host = test_config.get('host', '127.0.0.1') port = test_config.get('port', DEFAULT_PORT) userpass = (test_config.get('username', 'testsuite'), test_config.get('password', 'testsuite')) # Create the REST interface and the WebSocket Factory self.ari = ARI(host, port=port, userpass=userpass) self.ari_factory = AriClientFactory(receiver=self, host=host, port=port, apps=self.apps, userpass=userpass) self._ws_event_handlers = [] self.timed_out = False self.asterisk_instances = test_config.get('asterisk-instances', 1) self.create_asterisk(count=self.asterisk_instances) def run(self): """Override of TestCase run Called when reactor starts and after all instances of Asterisk have started """ super(AriBaseTestObject, self).run() self.ari_factory.connect() def register_ws_event_handler(self, callback): """Register a callback for when an event is received over the WS :param callback The method to call when an event is received. This will have a single parameter (the event) passed to it """ self._ws_event_handlers.append(callback) def on_reactor_timeout(self): """Called when the reactor times out""" self.timed_out = True # Fail the tests if we have timed out self.set_passed(False) def on_ws_event(self, message): """Handler for WebSocket events :param message The WS event payload """ if self.timed_out: # Ignore messages received after timeout LOGGER.debug("Ignoring message received after timeout") return for handler in self._ws_event_handlers: handler(message) def on_ws_open(self, protocol): """Handler for WebSocket Client Protocol opened :param protocol The WS Client protocol object """ reactor.callLater(0, self._create_ami_connection) def on_ws_closed(self, protocol): """Handler for WebSocket Client Protocol closed :param protocol The WS Client protocol object """ LOGGER.debug("WebSocket connection closed...") def _create_ami_connection(self): """Create the AMI connection""" self.create_ami_factory(count=self.asterisk_instances) class AriTestObject(AriBaseTestObject): """Class that acts as a Test Object in the pluggable module framework""" def __init__(self, test_path='', test_config=None): """Constructor for a test object :param test_path The full path to the test location :param test_config The YAML test configuration """ super(AriTestObject, self).__init__(test_path, test_config) if test_config is None: # Meh, just use defaults test_config = {} self.iterations = test_config.get('test-iterations') self.test_iteration = 0 self.channels = [] if self.iterations is None: self.iterations = [{'channel': 'Local/s@default', 'application': 'Echo'}] def ami_connect(self, ami): """Override of AriBaseTestObject ami_connect Called when an AMI connection is made :param ami The AMI factory """ # only use the first ami instance if ami.id != 0: return ami.registerEvent('Newchannel', self._new_channel_handler) ami.registerEvent('Hangup', self._hangup_handler) self.execute_test() return ami def _new_channel_handler(self, ami, event): """Handler for new channels :param ami The AMI instance :param event The Newchannl event """ LOGGER.debug("Tracking channel %s", event['channel']) self.channels.append(event['channel']) return (ami, event) def _hangup_handler(self, ami, event): """Handler for channel hangup :param ami The AMI instance :param event Hangup event """ if event['channel'] not in self.channels: return (ami, event) LOGGER.debug("Removing tracking for %s", event['channel']) self.channels.remove(event['channel']) if len(self.channels) == 0: self.test_iteration += 1 self.execute_test() return (ami, event) def execute_test(self): """Execute the current iteration of the test""" if not isinstance(self.iterations, list): return if self.test_iteration == len(self.iterations): LOGGER.info("All iterations executed; stopping") self.stop_reactor() return iteration = self.iterations[self.test_iteration] if isinstance(iteration, list): for channel in iteration: self._spawn_channel(channel) else: self._spawn_channel(iteration) def _spawn_channel(self, channel_def): """Create a new channel""" # There's only one Asterisk instance, so just use the first AMI factory LOGGER.info("Creating channel %s", channel_def['channel']) if not self.ami[0]: LOGGER.warning("Error creating channel - no ami available") return deferred = self.ami[0].originate(**channel_def) deferred.addErrback(self.handle_originate_failure) class AriOriginateTestObject(AriTestObject): """Class that overrides AriTestObject origination to use ARI""" def __init__(self, test_path='', test_config=None): """Constructor for a test object :param test_path The full path to the test location :param test_config The YAML test configuration """ if test_config is None: test_config = {} if not test_config.get('test-iterations'): # preset the default test in ARI format to prevent post failure test_config['test-iterations'] = [{ 'endpoint': 'Local/s@default', 'channelId': 'testsuite-default-id', 'app': 'testsuite' }] super(AriOriginateTestObject, self).__init__(test_path, test_config) def _spawn_channel(self, channel_def): """Create a new channel""" # Create a channel using ARI POST to channel instead LOGGER.info("Creating channel %s", channel_def['endpoint']) self.ari.post('channels', **channel_def) class WebSocketEventModule(object): """Module for capturing events from the ARI WebSocket""" def __init__(self, module_config, test_object): """Constructor. :param module_config: Configuration dict parse from test-config.yaml. :param test_object: Test control object. """ self.ari = test_object.ari self.event_matchers = [ EventMatcher(self.ari, e, test_object) for e in module_config['events']] self.event_matchers.append(EventMatcher(self.ari, VALIDATION_MATCHER, test_object)) test_object.register_ws_event_handler(self.on_event) def on_event(self, event): """Handle incoming events from the WebSocket. :param event: Dictionary parsed from incoming JSON event. """ LOGGER.debug("Received event: %r", event.get('type')) matched = False for matcher in self.event_matchers: if matcher.on_event(event): matched = True if not matched: LOGGER.info("Event had no matcher: %r", event) class AriClientFactory(WebSocketClientFactory): """Twisted protocol factory for building ARI WebSocket clients.""" def __init__(self, receiver, host, apps, userpass, port=DEFAULT_PORT, timeout_secs=60): """Constructor :param receiver The object that will receive events from the protocol :param host: Hostname of Asterisk. :param apps: App names to subscribe to. :param port: Port of Asterisk web server. :param timeout_secs: Maximum time to try to connect to Asterisk. """ url = "ws://%s:%d/ari/events?%s" % \ (host, port, urllib.urlencode({'app': apps, 'api_key': '%s:%s' % userpass})) LOGGER.info("WebSocketClientFactory(url=%s)", url) WebSocketClientFactory.__init__(self, url, debug=True, protocols=['ari']) self.timeout_secs = timeout_secs self.attempts = 0 self.start = None self.receiver = receiver def buildProtocol(self, addr): """Make the protocol""" return AriClientProtocol(self.receiver, self) def clientConnectionFailed(self, connector, reason): """Doh, connection lost""" LOGGER.debug("Connection lost; attempting again in 1 second") reactor.callLater(1, self.reconnect) def connect(self): """Start the connection""" self.reconnect() def reconnect(self): """Attempt to reconnect the ARI WebSocket. This call will give up after timeout_secs has been exceeded. """ self.attempts += 1 LOGGER.debug("WebSocket attempt #%d", self.attempts) if not self.start: self.start = datetime.datetime.now() runtime = (datetime.datetime.now() - self.start).seconds if runtime >= self.timeout_secs: LOGGER.error(" Giving up after %d seconds", self.timeout_secs) raise Exception("Failed to connect after %d seconds" % self.timeout_secs) connectWS(self) class AriClientProtocol(WebSocketClientProtocol): """Twisted protocol for handling a ARI WebSocket connection.""" def __init__(self, receiver, factory): """Constructor. :param receiver The event receiver """ LOGGER.debug("Made me a client protocol!") self.receiver = receiver self.factory = factory def onOpen(self): """Called back when connection is open.""" LOGGER.debug("WebSocket Open") self.receiver.on_ws_open(self) def onClose(self, wasClean, code, reason): """Called back when connection is closed.""" LOGGER.debug("WebSocket closed(%r, %d, %s)", wasClean, code, reason) self.receiver.on_ws_closed(self) def onMessage(self, msg, binary): """Called back when message is received. :param msg: Received text message. """ LOGGER.debug("rxed: %s", msg) msg = json.loads(msg) self.receiver.on_ws_event(msg) class ARI(object): """Bare bones object for an ARI interface.""" def __init__(self, host, userpass, port=DEFAULT_PORT): """Constructor. :param host: Hostname of Asterisk. :param port: Port of the Asterisk webserver. """ self.base_url = "http://%s:%d/ari" % (host, port) self.userpass = userpass self.allow_errors = False def build_url(self, *args): """Build a URL from the given path. For example:: # Builds the URL for /channels/{channel_id}/answer ari.build_url('channels', channel_id, 'answer') :param args: Path segments. """ path = [str(arg) for arg in args] return '/'.join([self.base_url] + path) def get(self, *args, **kwargs): """Send a GET request to ARI. :param args: Path segements. :param kwargs: Query parameters. :returns: requests.models.Response :throws: requests.exceptions.HTTPError """ url = self.build_url(*args) LOGGER.info("GET %s %r", url, kwargs) return self.raise_on_err(requests.get(url, params=kwargs, auth=self.userpass)) def put(self, *args, **kwargs): """Send a PUT request to ARI. :param args: Path segements. :param kwargs: Query parameters. :returns: requests.models.Response :throws: requests.exceptions.HTTPError """ url = self.build_url(*args) LOGGER.info("PUT %s %r", url, kwargs) return self.raise_on_err(requests.put(url, params=kwargs, auth=self.userpass)) def post(self, *args, **kwargs): """Send a POST request to ARI. :param args: Path segements. :param kwargs: Query parameters. :returns: requests.models.Response :throws: requests.exceptions.HTTPError """ url = self.build_url(*args) LOGGER.info("POST %s %r", url, kwargs) return self.raise_on_err(requests.post(url, params=kwargs, auth=self.userpass)) def delete(self, *args, **kwargs): """Send a DELETE request to ARI. :param args: Path segements. :param kwargs: Query parameters. :returns: requests.models.Response :throws: requests.exceptions.HTTPError """ url = self.build_url(*args) LOGGER.info("DELETE %s %r", url, kwargs) return self.raise_on_err(requests.delete(url, params=kwargs, auth=self.userpass)) def request(self, method, *args, **kwargs): """ Send an arbitrary request to ARI. :param method: Method (get, post, delete, etc). :param args: Path segements. :param kwargs: Query parameters. :returns: requests.models.Response :throws: requests.exceptions.HTTPError """ url = self.build_url(*args) LOGGER.info("%s %s %r", method, url, kwargs) requests_method = getattr(requests, method) return self.raise_on_err(requests_method(url, params=kwargs, auth=self.userpass)) def set_allow_errors(self, value): """Sets whether error responses returns exceptions. If True, then error responses are returned. Otherwise, methods throw an exception on error. :param value True/False value for allow_errors. """ self.allow_errors = value def raise_on_err(self, resp): """Helper to raise an exception when a response is a 4xx or 5xx error. If allow_errors is True, then an exception is not raised. :param resp: requests.models.Response object :returns: resp """ if not self.allow_errors and resp.status_code / 100 != 2: LOGGER.error('%s (%d %s): %r', resp.url, resp.status_code, resp.reason, resp.text) resp.raise_for_status() return resp class ARIRequest(object): """ Object that issues ARI requests and valiates response """ def __init__(self, ari, config): self.ari = ari self.method = config['method'] self.uri = config['uri'] self.params = config.get('params') or {} self.body = config.get('body') self.instance = config.get('instance') self.delay = config.get('delay') self.expect = config.get('expect') self.headers = None if self.body: self.body = json.dumps(self.body) self.headers = {'Content-type': 'application/json'} def send(self, values): """Send this ARI request substituting the given values""" uri = var_replace(self.uri, values) url = self.ari.build_url(uri) requests_method = getattr(requests, self.method) response = requests_method( url, params=self.params, data=self.body, headers=self.headers, auth=self.ari.userpass) if self.expect: if response.status_code != self.expect: LOGGER.error('sent %s %s %s expected %s response %d %s', self.method, self.uri, self.params, self.expect, response.status_code, response.text) return False else: if response.status_code / 100 != 2: LOGGER.error('sent %s %s %s response %d %s', self.method, self.uri, self.params, response.status_code, response.text) return False LOGGER.info('sent %s %s %s response %d %s', self.method, self.uri, self.params, response.status_code, response.text) return response class EventMatcher(object): """Object to observe incoming events and match them against a config""" def __init__(self, ari, instance_config, test_object): self.ari = ari self.instance_config = instance_config self.test_object = test_object self.conditions = self.instance_config['conditions'] self.count_range = decode_range(self.instance_config.get('count')) self.count = 0 self.passed = True callback = self.instance_config.get('callback') if callback: module = __import__(callback['module']) self.callback = getattr(module, callback['method']) else: # No callback; just use a no-op self.callback = lambda *args, **kwargs: True request_list = self.instance_config.get('requests') or [] if isinstance(request_list, dict): request_list = [request_list] self.requests = [ARIRequest(ari, request_config) for request_config in request_list] test_object.register_stop_observer(self.on_stop) def on_event(self, message): """Callback for every received ARI event. :param message: Parsed event from ARI WebSocket. """ if self.matches(message): self.count += 1 # send any associated requests for request in self.requests: if request.instance and request.instance != self.count: continue if request.delay: reactor.callLater(request.delay, request.send, message) else: response = request.send(message) if response is False: self.passed = False # Split call and accumulation to always call the callback try: res = self.callback(self.ari, message, self.test_object) if res: return True else: LOGGER.error("Callback failed: %r", self.instance_config) self.passed = False except: LOGGER.error("Exception in callback: %s", traceback.format_exc()) self.passed = False return False def on_stop(self, *args): """Callback for the end of the test. :param args: Ignored arguments. """ if not self.count_range.contains(self.count): # max could be int or float('inf'); format with %r LOGGER.error("Expected %d <= count <= %r; was %d (%r)", self.count_range.min_value, self.count_range.max_value, self.count, self.conditions) self.passed = False self.test_object.set_passed(self.passed) def matches(self, message): """Compares a message against the configured conditions. :param message: Incoming ARI WebSocket event. :returns: True if message matches conditions; False otherwise. """ match = self.conditions.get('match') res = all_match(match, message) # Now validate the nomatch, if it's there nomatch = self.conditions.get('nomatch') if res and nomatch: res = not all_match(nomatch, message) return res class Range(object): """Utility object to handle numeric ranges (inclusive).""" def __init__(self, min_value=0, max_value=float("inf")): """Constructor. :param min: Minimum value of the range. :param max: Maximum value of the range. """ self.min_value = min_value self.max_value = max_value def contains(self, value): """Checks if the given value is within this Range. :param value: Value to check. :returns: True/False if v is/isn't in the Range. """ return self.min_value <= value <= self.max_value def decode_range(yaml): """Parse a range from YAML specification.""" if yaml is None: # Unspecified; receive at least one return Range(1, float("inf")) elif isinstance(yaml, int): # Need exactly this many events return Range(yaml, yaml) elif yaml[0] == '<': # Need at most this many events return Range(0, int(yaml[1:])) elif yaml[0] == '>': # Need at least this many events return Range(int(yaml[1:]), float("inf")) else: # Need exactly this many events return Range(int(yaml), int(yaml)) class ARIPluggableEventModule(object): """Subclass of ARIEventInstance that works with the pluggable event action module. """ def __init__(self, test_object, triggered_callback, config): """Setup the ARI event observer""" self.test_object = test_object self.triggered_callback = triggered_callback if isinstance(config, list): self.config = config else: self.config = [config] for event_description in self.config: event_description["expected_count_range"] =\ decode_range(event_description.get('count', 1)) event_description["event_count"] = 0 test_object.register_ws_event_handler(self.event_callback) test_object.register_stop_observer(self.on_stop) def event_callback(self, event): """Callback called when an event is received from ARI""" for event_description in self.config: match = event_description["match"] nomatch = event_description.get("nomatch", None) if not all_match(match, event): continue # Now validate the nomatch, if it's there if nomatch and all_match(nomatch, event): continue event_description["event_count"] += 1 self.triggered_callback(self, self.test_object.ari, event) def on_stop(self, *args): """Callback for the end of the test. :param args: Ignored arguments. """ for event_desc in self.config: if not event_desc["expected_count_range"]\ .contains(event_desc["event_count"]): # max could be int or float('inf'); format with %r LOGGER.error("Expected %d <= count <= %r; was %d (%r, !%r)", event_desc["expected_count_range"].min_value, event_desc["expected_count_range"].max_value, event_desc["event_count"], event_desc["match"], event_desc.get("nomatch", None)) self.test_object.set_passed(False) self.test_object.set_passed(True) PLUGGABLE_EVENT_REGISTRY.register("ari-events", ARIPluggableEventModule) class ARIPluggableRequestModule(object): """Pluggable ARI action module. """ def __init__(self, test_object, config): """Setup the ARI event observer""" self.test_object = test_object if not isinstance(config, list): config = [config] self.requests = [ARIRequest(test_object.ari, request_config) for request_config in config] self.count = 0 def run(self, triggered_by, source, extra): """Callback called when this action is triggered.""" self.count += 1 for request in self.requests: if request.instance and request.instance != self.count: continue if request.delay: reactor.callLater(request.delay, request.send, extra) else: request.send(extra) PLUGGABLE_ACTION_REGISTRY.register("ari-requests", ARIPluggableRequestModule) # vim:sw=4:ts=4:expandtab:textwidth=79 asterisk-testsuite-0.0.0+svn.5781/lib/python/asterisk/astcsv.py000066400000000000000000000165161242304700500243670ustar00rootroot00000000000000#!/usr/bin/env python """Asterisk CSV-based testing This module implements the basic CSV testing backend for things like CDR and CEL tests. Copyright (C) 2010, Digium, Inc. Terry Wilson This program is free software, distributed under the terms of the GNU General Public License Version 2. """ import unittest import sys import csv import re import logging LOGGER = logging.getLogger(__name__) class AsteriskCSVLine(object): "A single Asterisk call detail record" def __init__(self, fields, **kwargs): """Construct an Asterisk CSV entry. The arguments list definition must be in the same order that the arguments appear in the CSV file. They can, of course, be passed to __init__ in any order. AsteriskCSV will pass the arguments via a **dict. """ # make all arguments passed available as instance variables self.__dict__.update(kwargs) self.__columns = kwargs self.__fields = fields def match(self, other, silent=False, exact=(False, False)): """Matches if the subset of fields that exist in both records match. It is important to make sure that if you want to test against an empty field, that it is passed as an empty string and not left out of the initialization or passed as None. exact - Whether (self, other) contain exact strings (True) or regexes """ if exact == (False, False): LOGGER.error("Can't compare two regexes, that's silly") return False elif exact == (False, True): cmp_fn = (lambda x, y: re.match(str(x).lower() + '$', str(y).lower())) elif exact == (True, False): cmp_fn = (lambda x, y: re.match(str(y).lower() + '$', str(x).lower())) else: cmp_fn = (lambda x, y: str(x).lower() == str(y).lower()) for key, value in self.iteritems(): if None not in (value, other.get(key)) and not cmp_fn(value, other.get(key)): if not silent: LOGGER.warn("CSV MATCH FAILED, Expected: %s: '%s' " \ "Got: %s: '%s'" % (key, value, key, other.get(key))) return False return True def get(self, key): """Retrieve a value from the specified column key""" return self.__columns.get(key) def iteritems(self): """Iterate over the values in the columns""" return self.__columns.iteritems() def __str__(self): return ",".join(["\"%s\"" % (self.__dict__[x]) for x in self.__fields]) class AsteriskCSV(object): """A representation of an Asterisk CSV file""" def __init__(self, fname=None, records=None, fields=None, row_factory=None): """Initialize CSV records from an Asterisk CSV file""" self.filename = fname self.row_factory = row_factory if records: self.__records = records return self.__records = [] csvreader = None try: csvreader = csv.DictReader(open(self.filename, "r"), fields, ",") except IOError as (errno, strerror): LOGGER.error("IOError %d[%s] while opening file '%s'" % (errno, strerror, self.filename)) except: LOGGER.error("Unexpected error: %s" % (sys.exc_info()[0])) if not csvreader: LOGGER.error("Unable to open file '%s'" % (self.filename)) return for row in csvreader: record = self.row_factory(**row) self.__records.append(record) def __len__(self): return len(self.__records) def __getitem__(self, key): return self.__records.__getitem__(key) def __iter__(self): return self.__records.__iter__() def match(self, other, partial=False): """Compares the length of self and other AsteriskCSVs and then compares each record""" if not partial and (len(self) != len(other)): LOGGER.warn("CSV MATCH FAILED, different number of records, " \ "self=%d and other=%d" % (len(self), len(other))) return False def match_order(list_a, list_b, cmp_func): """Utility function that attempts to find out whether list_a and list_b have a single ordering match or if there is more than one. >>> f = (lambda x, y: x == y) >>> match_order(['A', 'B', 'C'], ['C', 'B', 'X'], f) () >>> match_order(['A', 'B', 'C'], ['C', 'B', 'A'], f) ((2, 1, 0),) >>> match_order(['A', 'B', 'C', 'B'], ['C', 'B', 'A', 'B'], f) ((2, 1, 0, 3), (2, 3, 0, 1)) """ if not partial: assert len(list_a) == len(list_b) size = len(list_a) # attempt two orderings: forward and reversed guess_orders = (range(size), list(reversed(range(size)))) found_orders = [] for guess_order in guess_orders: found_order = [] for a_item in range(size): for b_item in guess_order: if cmp_func(list_a[a_item], list_b[b_item]): found_order.append(b_item) guess_order.remove(b_item) break else: # no match at all.. return () found_orders.append(tuple(found_order)) if found_orders[0] != found_orders[1]: return tuple(found_orders) return (found_orders[0],) # If there is a filename, then we know that the fields are just text, # and not regexes. We need to know this when we are matching individual # rows so we know whether it is self or other that has the text we are # matching against. If both are file-based, then we know not to use # regexes at all and just test equality exactness = (bool(self.filename), bool(other.filename)) # Use the match_order function to see if there either is (a) no match, # or (b) a single match or (c) several matches. In the latter case, the # regexes should probably be chosen more carefully. matches = match_order(self, other, (lambda x, y: x.match(y, silent=True, exact=exactness))) if len(matches) == 0: # Bah.. no match. Loop over the records in the normal order and # have it complain immediately. for i, item in enumerate(self): if not item.match(other[i], exact=exactness): LOGGER.warn("Failed to match entry %d" % (i,)) return False assert False elif len(matches) == 1: pass # joy! elif len(matches) > 1: LOGGER.warn("More than one CSV permutation results in success") return True def __str__(self): return "\n".join([str(item) for item in self.__records]) def empty(self): """Empty out the records""" try: open(self.filename, "w").close() except: LOGGER.warn("Unable to empty CSV file %s" % (self.filename)) if __name__ == '__main__': unittest.main() # vim:sw=4:ts=4:expandtab:textwidth=79 asterisk-testsuite-0.0.0+svn.5781/lib/python/asterisk/asterisk.py000077500000000000000000001013671242304700500247130ustar00rootroot00000000000000#!/usr/bin/env python """Asterisk Instances in Python. This module provides an interface for creating instances of Asterisk from within python code. Copyright (C) 2010-2013, Digium, Inc. Russell Bryant This program is free software, distributed under the terms of the GNU General Public License Version 2. """ import sys import os import time import shutil import logging import test_suite_utils from config import ConfigFile from version import AsteriskVersion from twisted.internet import reactor, protocol, defer, utils, error LOGGER = logging.getLogger(__name__) class AsteriskCliCommand(object): """Class that manages an Asterisk CLI command.""" def __init__(self, host, cmd): """Create a new Asterisk CLI Protocol instance This class wraps an Asterisk instance that executes a CLI command against another instance of Asterisk. Keyword Arguments: host The host this CLI instance will connect to cmd List of command arguments to spawn. The first argument must be the location of the Asterisk executable; each subsequent argument should define the CLI command to run and the instance of Asterisk to run it against. """ self.host = host self._cmd = cmd self.cli_cmd = cmd[4] self.exitcode = -1 self.output = "" self.err = "" self._deferred = None def execute(self): """Execute the CLI command. Returns a deferred that will be called when the operation completes. The parameter to the deferred is this object. """ def __cli_output_callback(result): """Callback from getProcessOutputAndValue""" self._set_properties(result) LOGGER.debug("Asterisk CLI %s exited %d" % (self.host, self.exitcode)) if self.err: LOGGER.debug(self.err) if self.exitcode: self._deferred.errback(self) else: self._deferred.callback(self) def __cli_error_callback(result): """Errback from getProcessOutputAndValue""" self._set_properties(result) LOGGER.warning("Asterisk CLI %s exited %d with error: %s" % (self.host, self.exitcode, self.err)) if self.err: LOGGER.debug(self.err) self._deferred.errback(self) self._deferred = defer.Deferred() deferred = utils.getProcessOutputAndValue(self._cmd[0], self._cmd, env=os.environ) deferred.addCallbacks(callback=__cli_output_callback, errback=__cli_error_callback,) return self._deferred def _set_properties(self, result): """Set the properties based on the result of the getProcessOutputAndValue call""" self.output, self.err, self.exitcode = result class AsteriskProtocol(protocol.ProcessProtocol): """Class that manages an Asterisk instance""" def __init__(self, host, stop_deferred): """Create an AsteriskProtocol object Create an AsteriskProtocol object, which manages the interactions with the Asterisk process Keyword Arguments: host - the hostname or address of the Asterisk instance stop_deferred - a twisted Deferred object that will be called when the process has exited """ self.output = "" self._host = host self.exitcode = 0 self.exited = False self._stop_deferred = stop_deferred def outReceived(self, data): """Override of ProcessProtocol.outReceived""" self.output += data def connectionMade(self): """Override of ProcessProtocol.connectionMade""" LOGGER.debug("Asterisk %s - connection made" % (self._host)) def errReceived(self, data): """Override of ProcessProtocol.errReceived""" LOGGER.warn("Asterisk %s received error: %s" % (self._host, data)) def processEnded(self, reason): """Override of ProcessProtocol.processEnded""" message = "" if reason.value and reason.value.exitCode: message = "Asterisk %s ended with code %d" % \ (self._host, reason.value.exitCode,) self.exitcode = reason.value.exitCode else: message = "Asterisk %s ended " % self._host try: # When Asterisk gets itself terminated with a KILL signal, this may # (or may not) ever get called, in which case the Asterisk object # itself that is terminating this process will attempt to raise the # stop deferred. Prevent calling the object twice. if not self._stop_deferred.called: self._stop_deferred.callback(message) except defer.AlreadyCalledError: LOGGER.warning("Asterisk %s stop deferred already called" % self._host) self.exited = True class Asterisk(object): """An instance of Asterisk. This class assumes that Asterisk has been installed on the system. The installed version of Asterisk will be mirrored for this dynamically created instance. Instantiate an Asterisk object to create an instance of Asterisk from within python code. Multiple instances of Asterisk are allowed. However, custom configuration will need to be provided to prevent some modules from conflicting with each other. For example, modules that listen for network connections will need to be configured to use different ports on each instance of Asterisk. To set values for the [options] section in asterisk.conf, provide a dictionary to the ast_conf_options parameter. The key should be the option name and the value is the option's value to be written out into asterisk.conf. If AST_TEST_ROOT is unset (the default): BINARY = /usr/sbin/asterisk (or found in PATH) SOURCE_ETC_DIR = /etc/asterisk WORK_DIR = /tmp/asterisk-testsuite If it is set: BINARY = AST_TEST_ROOT/usr/sbin/asterisk SOURCE_ETC_DIR = AST_TEST_ROOT/etc/asterisk WORK_DIR = AST_TEST_ROOT/tmp This allows you to run tests in a separate environment and without root powers. Note that AST_TEST_ROOT has to be reasonably short (symlinked in /tmp?) so we're not running into the asterisk.ctl AF_UNIX limit. """ def compare_free_space(x, y): # statvfs can return a long; comparison functions must return an # int. Hence the checks that occur here. blocks_avail = os.statvfs(y).f_bavail - os.statvfs(x).f_bavail if (blocks_avail > 0): return 1 elif (blocks_avail < 0): return -1 else: return 0 localtest_root = os.getenv("AST_TEST_ROOT") if localtest_root: # Base location of the temporary files created by the testsuite test_suite_root = os.path.join(localtest_root, "tmp") # The default etc directory for Asterisk default_etc_directory = os.path.join(localtest_root, "etc/asterisk") else: # select tmp path with most available space best_tmp = sorted(['/tmp', '/var/tmp'], cmp=compare_free_space)[0] # Base location of the temporary files created by the testsuite test_suite_root = best_tmp + "/asterisk-testsuite" # The default etc directory for Asterisk default_etc_directory = "/etc/asterisk" def __init__(self, base=None, ast_conf_options=None, host="127.0.0.1"): """Construct an Asterisk instance. Keyword Arguments: base -- This is the root of the files associated with this instance of Asterisk. By default, the base is "/tmp/asterisk-testsuite" directory. Given a base, it will be appended to the default base directory. Example Usage: self.asterisk = Asterisk(base="manager/login") """ self._start_deferred = None self._stop_deferred = None self._stop_cancel_tokens = [] self.directories = {} self.ast_version = AsteriskVersion() self.process_protocol = None self.process = None self.astetcdir = "" self.original_astmoddir = "" if base is not None: self.base = base else: self.base = Asterisk.test_suite_root if self.localtest_root: self.ast_binary = self.localtest_root + "/usr/sbin/asterisk" else: ast_binary = test_suite_utils.which("asterisk") self.ast_binary = ast_binary or "/usr/sbin/asterisk" self.host = host self._ast_conf_options = ast_conf_options self._directory_structure_made = False self._configs_installed = False self._configs_set_up = False # Find the system installed asterisk.conf ast_confs = [ os.path.join(self.default_etc_directory, "asterisk.conf"), "/usr/local/etc/asterisk/asterisk.conf", ] self._ast_conf = None for config in ast_confs: if os.path.exists(config): self._ast_conf = ConfigFile(config) break if self._ast_conf is None: msg = "Unable to locate asterisk.conf in any known location" LOGGER.error(msg) raise Exception(msg) # Set which astxxx this instance will be i = 1 while True: if not os.path.isdir("%s/ast%d" % (self.base, i)): self.base = "%s/ast%d" % (self.base, i) break i += 1 # Get the Asterisk directories from the Asterisk config file for cat in self._ast_conf.categories: if cat.name == "directories": for (var, val) in cat.options: self.directories[var] = val # self.original_astmoddir is for dependency checking only if "astmoddir" in self.directories: if self.localtest_root: self.original_astmoddir = "%s%s" % ( self.localtest_root, self.directories["astmoddir"]) else: self.original_astmoddir = self.directories["astmoddir"] def start(self, deps=None): """Start this instance of Asterisk. Returns: A deferred object that will be called when Asterisk is fully booted. Example Usage: asterisk.start() Note that calling this will install the default testsuite config files, if they have not already been installed """ def __start_asterisk_callback(cmd): """Begin the Asterisk startup cycle""" self.process_protocol = AsteriskProtocol(self.host, self._stop_deferred) self.process = reactor.spawnProcess(self.process_protocol, cmd[0], cmd, env=os.environ) # Begin the wait fully booted cycle self.__start_asterisk_time = time.time() reactor.callLater(1, __execute_wait_fully_booted) def __execute_wait_fully_booted(): """Send the CLI command waitfullybooted""" cli_deferred = self.cli_exec("core waitfullybooted") cli_deferred.addCallbacks(__wait_fully_booted_callback, __wait_fully_booted_error) def __wait_fully_booted_callback(cli_command): """Callback for CLI command waitfullybooted""" if "Asterisk has fully booted" in cli_command.output: msg = "Successfully started Asterisk %s" % self.host self._start_deferred.callback(msg) else: LOGGER.debug("Asterisk core waitfullybooted failed " + "with output '%s', attempting again..." % cli_command.output) reactor.callLater(1, __execute_wait_fully_booted) return cli_command def __wait_fully_booted_error(cli_command): """Errback for CLI command waitfullybooted""" if time.time() - self.__start_asterisk_time > 5: msg = "Asterisk core waitfullybooted for %s failed" % self.host LOGGER.error(msg) self._start_deferred.errback(Exception(msg)) else: msg = "Asterisk core waitfullybooted failed, attempting again" LOGGER.debug(msg) reactor.callLater(1, __execute_wait_fully_booted) return cli_command self.install_configs(os.getcwd() + "/configs", deps) self._setup_configs() cmd = [ self.ast_binary, "-f", "-g", "-q", "-m", "-n", "-C", "%s" % os.path.join(self.astetcdir, "asterisk.conf") ] # Make the start/stop deferreds - this method will return # the start deferred, and pass the stop deferred to the AsteriskProtocol # object. The stop deferred will be raised when the Asterisk process # exits self._start_deferred = defer.Deferred() self._stop_deferred = defer.Deferred() # Asterisk will attempt to use built in configuration information if # it can't find the configuration files that are being installed - which # can happen due to the files being created due to a copy operation. # If that happens, the test will fail - wait a second to give # Asterisk time to come up fully reactor.callLater(0, __start_asterisk_callback, cmd) return self._start_deferred def stop(self): """Stop this instance of Asterisk. This function is used to stop this instance of Asterisk. Returns: A deferred that can be used to detect when Asterisk exits, or if it fails to exit. Example Usage: asterisk.stop() """ def __cancel_stops(reason): """Cancel all stop actions - called when the process exits""" for token in self._stop_cancel_tokens: try: if token.active(): token.cancel() except error.AlreadyCalled: # Ignore if we already killed it pass return reason def __send_stop_gracefully(): """Send a core stop gracefully CLI command""" LOGGER.debug('sending stop gracefully') if self.ast_version < AsteriskVersion("1.6.0"): cli_deferred = self.cli_exec("stop gracefully") else: cli_deferred = self.cli_exec("core stop gracefully") cli_deferred.addCallbacks(__stop_gracefully_callback, __stop_gracefully_error) def __stop_gracefully_callback(cli_command): """Callback handler for the core stop gracefully CLI command""" LOGGER.debug("Successfully stopped Asterisk %s" % self.host) reactor.callLater(0, __cancel_stops, None) return cli_command def __stop_gracefully_error(cli_command): """Errback for the core stop gracefully CLI command""" LOGGER.warning("Asterisk graceful stop for %s failed" % self.host) return cli_command def __send_kill(): """Check to see if the process is running and kill it with fire""" try: if not self.process_protocol.exited: LOGGER.warning("Sending KILL to Asterisk %s" % self.host) self.process.signalProcess("KILL") except error.ProcessExitedAlready: # Pass on this pass # If you kill the process, the ProcessProtocol may never get # the note that its dead. Call the stop callback to notify everyone # that we did indeed kill the Asterisk instance. try: # Attempt to signal the process object that it should lose its # connection - it may already be gone however. self.process.loseConnection() except: pass try: if not self._stop_deferred.called: self._stop_deferred.callback("Asterisk %s KILLED" % self.host) except defer.AlreadyCalledError: LOGGER.warning("Asterisk %s stop deferred already called" % self.host) def __process_stopped(reason): """Generic callback that raises the stopped deferred subscribers use to know that the process has exited""" self._stop_deferred.callback(reason) return reason if self.process_protocol.exited: try: if not self._stop_deferred.called: self._stop_deferred.callback( "Asterisk %s stopped prematurely" % self.host) except defer.AlreadyCalledError: LOGGER.warning("Asterisk %s stop deferred already called" % self.host) else: # Schedule a kill. If we don't gracefully shut down Asterisk, this # will ensure that the test is stopped. self._stop_cancel_tokens.append(reactor.callLater(10, __send_kill)) # Start by asking to stop gracefully. __send_stop_gracefully() self._stop_deferred.addCallback(__cancel_stops) return self._stop_deferred def get_path(self, astdirkey, *paths): """Join paths using the correct prefix for the current instance. Keyword Arguments: astdirkey This argument must match a directory key from asterisk.conf. *paths This is a list of paths to be appended using os.path.join. Example Usage: asterisk.get_path("astlogdir", "cdr-csv", "Master.csv") """ if not astdirkey in self.directories: msg = "Directory '%s' not found in asterisk.conf" % astdirkey LOGGER.error(msg) raise Exception(msg) return os.path.join(self.base + self.directories[astdirkey], *paths) def install_configs(self, cfg_path, deps=None): """Installs all files located in the configuration directory for this instance of Asterisk. By default, the configuration used will be whatever is found in the system install of Asterisk. However, custom configuration files to be used only by this instance can be provided via this API call. Keyword Arguments: cfg_path This argument must be the path to the configuration directory to be installed into this instance of Asterisk. Only top-level files will be installed, sub directories will be ignored. Example Usage: asterisk.install_configs("tests/my-cool-test/configs") Note that this will install the default testsuite config files, if they have not already been installed. """ self._make_directory_structure() cur_cfg_path = "%s/configs" % os.getcwd() if self._configs_installed and cfg_path == cur_cfg_path: return if not self._configs_installed and cfg_path != cur_cfg_path: # Do a one-time installation of the base configs self.install_configs("%s/configs" % os.getcwd()) # the default modules.conf should be installed now, so append # conflicts this can be overriden by a test specific modules.conf self._append_modules_conf(deps) self._configs_installed = True if not os.access(cfg_path, os.F_OK): return for fname in os.listdir(cfg_path): target = "%s/%s" % (cfg_path, fname) if os.path.isfile(target): self.install_config(target) def install_config(self, cfg_path, target_filename=None): """Install a custom configuration file for this instance of Asterisk. By default, the configuration used will be whatever is found in the system install of Asterisk. However, custom configuration files to be used only by this instance can be provided via this API call. Note: If a sub-directory is found to have the same name as the running instance, install_config() will use the sub-directories version in place of the top-level version. For example, testsuite is running a test against 1.4 (branch-1.4): configs/manager.conf configs/sip.conf configs/branch-1.4/sip.conf Because the sip.conf file exists in the branch-1.4 directory, it will be used in place of the top-level sip.conf. As for the manager.conf file, because it does not exists in the branch-1.4 direcory, the top-level manager.conf will be used. Keyword Arguments: cfg_path -- This argument must be the path to the configuration file to be installed into the directory tree for this instance of Asterisk. target_filename -- If this argument is specified, the config file provided will be installed with this filename in the Asterisk etc directory. Use this if the file for cfg_path doesn't match the name of the configuration needed by Asterisk. Example Usage: asterisk.install_config("tests/my-cool-test/configs/manager.conf") asterisk.install_config("tests/my-cool-test/replacement_manager.conf", target_filename="manager.conf") """ self._make_directory_structure() if not os.path.exists(cfg_path): LOGGER.error("Config file '%s' does not exist" % cfg_path) return tmp = "%s/%s/%s" % (os.path.dirname(cfg_path), self.ast_version.branch, os.path.basename(cfg_path)) if os.path.exists(tmp): cfg_path = tmp if (target_filename): target_path = os.path.join(self.astetcdir, target_filename) else: target_path = os.path.join(self.astetcdir, os.path.basename(cfg_path)) if os.path.exists(target_path): os.remove(target_path) try: shutil.copyfile(cfg_path, target_path) except shutil.Error: LOGGER.warn("'%s' and '%s' are the same file" % (cfg_path, target_path)) except IOError: LOGGER.warn("The destination is not writable '%s'" % target_path) def _overwrite_file(self, filename, values): """Overwrite a particular config file Keyword Arguments: filename The name of the conf file to overwrite values A list of key/value pair tuples to write to the file """ target_filename = os.path.join(self.astetcdir, filename) if not os.path.exists(target_filename): LOGGER.error("File '%s' does not exists" % filename) return try: ofile = open(target_filename, "w") except IOError: LOGGER.error("Failed to open %s" % target_filename) return except: LOGGER.error("Unexpected error: %s" % sys.exc_info()[0]) return for (var, val) in values: ofile.write('%s = %s\n' % (var, val)) ofile.close() def cli_originate(self, argstr): """Starts a call from the CLI and links it to an application or context. Must pass a valid argument string in the following form: application appdata extension @ If no context is specified, the 'default' context will be used. If no extension is given, the 's' extension will be used. Keyword Arguments: argstr Arguments to be passed to the originate Returns: A deferred object that can be used to listen for command completion Example Usage: asterisk.originate("Local/a_exten@context extension b_exten@context") """ args = argstr.split() raise_error = False if len(args) != 3 and len(args) != 4: raise_error = True LOGGER.error("Wrong number of arguments.") if args[1] != "extension" and args[1] != "application": raise_error = True LOGGER.error('2nd argument must be "extension" or "application"') if args[0].find("/") == -1: raise_error = True LOGGER.error('Channel dial string must be in the form "tech/data".') if raise_error is True: raise Exception("Cannot originate call!\n" "Argument string must be in one of these forms:\n" " application appdata\n" " extension @") if self.ast_version < AsteriskVersion("1.6.2"): return self.cli_exec("originate %s" % argstr) else: return self.cli_exec("channel originate %s" % argstr) def cli_exec(self, cli_cmd): """Execute a CLI command on this instance of Asterisk. Keyword Arguments: cli_cmd The command to execute. Returns: A deferred object that will be signaled when the process has exited Example Usage: asterisk.cli_exec("core set verbose 10") """ cmd = [ self.ast_binary, "-C", "%s" % os.path.join(self.astetcdir, "asterisk.conf"), "-rx", "%s" % cli_cmd ] LOGGER.debug("Executing %s ..." % cmd) cli_protocol = AsteriskCliCommand(self.host, cmd) return cli_protocol.execute() def _make_directory_structure(self): """ Mirror system directory structure """ if self._directory_structure_made: return # Make the directory structure if not available if not os.path.exists(self.base): os.makedirs(self.base) dir_cat = None for cat in self._ast_conf.categories: if cat.name == "directories": dir_cat = cat if dir_cat is None: LOGGER.error("Unable to discover dir layout from asterisk.conf") raise Exception("Unable to discover dir layout from asterisk.conf") self._gen_ast_conf(dir_cat, self._ast_conf_options) # Cache mirrored dirs to speed up creation. Generally you'll have # /var/lib/asterisk for more than one dir_cat option, and that happens # to be the largest dir too (with lots of sounds). cache = set() for (var, val) in dir_cat.options: # We cannot simply skip ``val`` here if we already processed it. # Some dirs are exempt from copying, based on ``var``. self.__mirror_dir(var, val, cache) self._directory_structure_made = True def _setup_configs(self): """Perform any post-installation manipulation of the config files""" if self._configs_set_up: return self._setup_manager_conf() self._configs_set_up = True def _setup_manager_conf(self): """Forcibly create a manger.general.conf.inc file for non-localhosts""" if self.host == '127.0.0.1': return values = [('bindaddr', self.host), ] self._overwrite_file("manager.general.conf.inc", values) def _gen_ast_conf(self, dir_cat, ast_conf_options): """Generate a default asterisk.conf""" for (var, val) in dir_cat.options: if var == "astetcdir": self.astetcdir = "%s%s" % (self.base, val) os.makedirs(self.astetcdir) local_ast_conf_path = os.path.join(self.astetcdir, "asterisk.conf") try: ast_file = open(local_ast_conf_path, "w") except IOError: LOGGER.error("Failed to open %s" % local_ast_conf_path) return except: LOGGER.error("Unexpected error: %s" % sys.exc_info()[0]) return for cat in self._ast_conf.categories: ast_file.write("[%s]\n" % cat.name) if cat.name == "directories": for (var, val) in cat.options: ast_file.write("%s = %s%s\n" % (var, self.base, val)) elif cat.name == "options": ast_file.write("#include \"%s/asterisk.options.conf.inc\"\n" % (self.astetcdir)) if ast_conf_options: for (var, val) in ast_conf_options.iteritems(): ast_file.write("%s = %s\n" % (var, val)) for (var, val) in cat.options: if not ast_conf_options or var not in ast_conf_options: ast_file.write("%s = %s\n" % (var, val)) else: for (var, val) in cat.options: ast_file.write("%s = %s\n" % (var, val)) ast_file.write("\n") ast_file.close() def _get_module_conflicts(self, deps): """Attempt to get modules this test conflicts with""" if not deps: return [] conflicts = [] conflict_file_name = os.getcwd() + '/configs/' + 'conflicts.txt' try: with open(conflict_file_name, 'r') as conflict_file: for line in conflict_file: line = line.strip() if not line or line[0] == ';': continue key, val = line.split('=') if next((dep for dep in deps if key == dep.name), None): for value in val.split(','): conflicts.append(value.strip()) except: pass return conflicts def _append_modules_conf(self, deps): """Prevent modules from loading based on dependency conflicts""" conflicts = self._get_module_conflicts(deps) if not conflicts: return modules_conf = os.path.join(self.astetcdir, "modules.conf") try: with open(modules_conf, "a") as modules_file: for conflict in conflicts: modules_file.write('noload => %s\n' % conflict) except IOError: LOGGER.error("Failed to open %s" % modules_conf) return except: LOGGER.error("Unexpected error: %s" % sys.exc_info()[0]) return def __mirror_dir(self, ast_dir_name, ast_dir_path, cache): """Mirror an Asterisk directory for a test run""" self._makedirs(ast_dir_path) dirs_only = ["astrundir", "astlogdir", "astspooldir"] if ast_dir_name in dirs_only: return blacklist = ["astdb", "astdb.sqlite3"] if ast_dir_path in cache: return cache.add(ast_dir_path) # If we're running with a separate localroot, the paths in # AST_TEST_ROOT/etc/asterisk/asterisk.conf are still short; e.g. # /etc/asterisk. We suffix AST_TEST_ROOT here to get the real source # dir. if self.localtest_root: ast_real_dir_path = self.localtest_root + ast_dir_path else: ast_real_dir_path = ast_dir_path for dirname, dirnames, filenames in os.walk(ast_real_dir_path): assert dirname[0] == "/" for filename in filenames: # Shorten the dirname for inclusion into the new path. short_dirname = dirname if self.localtest_root: short_dirname = dirname[len(self.localtest_root):] assert short_dirname[0] == "/" target = "%s/%s/%s" % (self.base, short_dirname[1:], filename) if os.path.lexists(target) or filename in blacklist: continue os.symlink(os.path.join(ast_dir_path, dirname, filename), target) def _makedirs(self, ast_dir_path): """Make the Asterisk directories""" # If we're running with a separate localroot, the paths in # AST_TEST_ROOT/etc/asterisk/asterisk.conf are still short; e.g. # /etc/asterisk. We suffix AST_TEST_ROOT here to get the real source # dir. if self.localtest_root: ast_real_dir_path = self.localtest_root + ast_dir_path else: ast_real_dir_path = ast_dir_path target_dir = "%s%s" % (self.base, ast_dir_path) if not os.path.exists(target_dir): os.makedirs(target_dir) for dirname, dirnames, filenames in os.walk(ast_real_dir_path): # Shorten the dirname for inclusion into the new path. short_dirname = dirname if self.localtest_root: short_dirname = dirname[len(self.localtest_root):] for dirname in dirnames: self._makedirs(os.path.join(target_dir, short_dirname, dirname)) # vim: set ts=8 sw=4 sts=4 et ai tw=79: asterisk-testsuite-0.0.0+svn.5781/lib/python/asterisk/audio.ulaw000066400000000000000000000501171242304700500245000ustar00rootroot00000000000000~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|xgmmsZG:2/2:aóSG<955887:?Njź?0+((),.16;E]V>317JɲNA:6/,(%#%*4L˻UFMñfUI?7.)$!"(2JͺºN;2-*)(),0;Ia¿|̾fB81.,+++.19D]ſ~jhj̽i@4.,,-/3338@U{\XZm?2--/43/.18AV`OHGNoϽV:0/3970.17931259>KZoZOKKOWiƺI??A=952348CHJOSQ\fayiͿ]LC<;87768::<=?BHOX]eȾcOF?;87553479;>DGR\ļhME@<754333589=GO]naOB<:73324678;?IXZƿgSJ=863442778>>GN[g[K?;83344369;>?GKOm˾WIA8755339869>?LNW˿`MB=:77699:<:>BQcʽ^PB<75589:;>AAFLĽTE<862201:=AEE_Ȼ^KF>71-+*)+/5:CNiӾoQH=4,'%%'),/7?VѾS=0*&&&'*-38F]?1*&$#$(-6Agƺo@5,(%"!#'/>frG;2+'" "&.9H̺Y?70+($"$*6CֹP>2,'&%$%)2CǰL6,*('$!%,6A˱v?1+*'$#$*28MA6-+&"#$&+1=±SB4*'$%##)/4_M9,*&$$$)/1XH6,*$"$$(/1\D7,)$"$#(/0ŨH4,)#"%#)/2μ¤H2+*!"%#)-6ζ>1-'"&$#,,D᫦V9/,#$&"(-/ο>6,'#%##++B﬩a=/-$%&!(+0nK4.'$'"%*+VuϪ\;/+$&$#)*=u@2.%&%!((6^\I3/'&%"&'2MP`61)&&"&&/GHY;2)(&#&&0CG];3*)&$'&2?FR=2)*%%&&6=MJ=/)*$%&'8=]G>.+)#'%*::ẼF>,-(%(%.9>ɾ]I6,-&&''2;ILB0++%&')7Ӽ]I4-,'&'(/=IļO>/-)&&&*6@rL6-+'&%',9Eٹ^E2-*'&&)/=LɸU>.-)&&&)2@YM9.,(&&'+6Fq^L4-,'&&',:IܷWH1-+'&&',=GڵSI1,+&%&'-=GҴLH2++'%&',>FڲNF5++'$&&*=G峳QH9++($%')8JZ??,)+#%''1KND?2)*'"''*EO];=+'+#"((1_YC96(()"$*)gI4,*%%%'+.:\J=/*(%&'+.8G൴A7.+'%)+,4AN洯I5.-*&+//9JlݾpY?5//-,19=JƲ¿WYP>4786:@OZbǾo[LAFDACIRLT_^poZ[RBBHIMFHMMLNŽUIIH>?ABA>ION`ƿnLFFEB<=FB>FP_pžOBFI@==CD@MVOmɾ^LMC>B:CF>DLYPj¿ɿIHL>>>BCIMBOhiʿnXG??A?;>>?=>@I@UhbVPHBFE@FKQQPYgd|YJOiLHJM[MKZ~YsZLTNLJINMRM^waX_]JHMKSNOnnlj]OP[^QTdfyi[cVZ]V[hmlxxjpipmkbdv|wiqoyozbd[OZjqZ\hhYg[rhTR\^JPWSdhix\ifWSY[\MRhklYng~k]UOLIBEL@HEFp_Yӿ[SOG@<98;84;AEHGOoνR=::7456:;=>CONNDA@ZR;1.,)*,5=AJQbQMONB>˰S5*'''&'.BeNHC3?Y8*%&&&',>ֹE?;5,6G2%!'(*,5^T7/..,&=RBA2)!#,5=A縭Z?/+,/.,1764.)%+9Qmzǵ{SM>1-,3663VE1112+)0BƹUH=8/./:?>9OK0,-/-+-:žNNMH:3/05;==9֦>.((-.03BֹiF<[D7,).6@FLcͼcOI@>F]iNIJ[R@;fB5*'*1@TŹwK@>=?JckLIHA;54JE8,''+8O^?::<;644?^lRC>ImYPKG>:8>A:5/09KWA>CXz]TNHB=?GVfy˱~L@F?90,-4D_ºQEFSihQF?BGMMJFLZȴWJLD;/,,3GǾsE9:ATl\NA@CMSTMOdŴQ=<97/-,/;P\E>?IUc^TLMP_zֽN:8772/.0:N]D::>J\mhYTZz~ƶN;<<@=844966;E]rXJHHOXcd^ZX_ol[X]gнnMGEA<7338AX~YKHINUYXUV[kk_]^bjٿPJHD<6116?TmNDACKR]^_]exi[YZ\iܿPLIC<5118AY[MHHLPYZ][ahwc_X[V_³XOKE<40/5=NteOKJMPVWY[`kz~hWRRUpϺ^TI@82/17?OcaUMMMPUX]^cdn{t]TQUoκm[KA94139@LZ{dSMNSZ\[WSTYfc\YV[wͺtcOD9414:BNZ{WNNSWXPLJJO]qleoӿybWJ=7248?KZwdUOUYZQKIIMVfnjm˼_K?:89<@GN[kfhh_PKGHKQZkºwVF@>>ADHKSYi~wimlyl\QPV[_gtĿmXNGDFGIKOSTXchjjkejikl^XX[]cow_TLIFFGHKLNU^cfmfh{nsk^[djihj~osp_TOKIGGIIKMPYZ_ksnxgfsnedde]hjevgb[KIIIIILNMRWU]^\]_dodgissh]\_[TSZOJQWXONSXZQ[f^]tsl~}zxo}tZX]UNSTNVWN[X`fdwkxdyx~v[jz^V`dVS]e_]gjemyrsq|ya|fn~}uuklcgfgpc^|h^xjnv|gvkymjQgigmu`lisvolswf}XZz__llmi~}zffi]fijb^mlxVpsntrfdo{mjwX~t_ou|}r_ae`jhZ]^]^ocqznsxjznz~l_fjjv|snw_kaho_xvilkjqlwpsflsk]_m~{hh}sgjntor{d`bvzyt}zljwkdhifglspoywqxsokouqru{{u}z~x}{}xknzuedtumuwrtnryo~zsxz{|z{rs{uqoutqwz|vv}zy}x~xutvusvyzstzxxy~vrsqoqqv|ru|wsv|}xrsssrruutpswy~}~~}}z{zzyxy|}}~~|~~~~}pzyts|t|~x~}rzzwtuuwsv}zzz~zr{~|w|~~|{yuwtzzqzzw}{}~}|}}}|}~{|||~~|{|yz|yxuvvxxz}{~~~|xzywwxxz|~~~~~~}||}{{~~}|{}~|{|zy{}}zz{yy{{{}}~}}|}}|~}~}~}~{xxy{yz{{||~~|{}~~~}||}~~}|{||{||{{{{{{|~~}}~~}||}~~~}}||}~~~~~|{|{{zyyzz{}~~~}|{{z{||||~~}||||{{{{|||~~~~~}}|{|||{zz|~~~}}}~~~~}{{{}|z{|{||~~~}||{|~~|~yyyy|{{~~||yz|yx}zy|{~{v|}~ywz}tr~tlk|ks~rpqldusmjy{oqw{vxn{w|y~mw{tnou{wsronrvkpvyvxfzkugjmkxnjVcdfg\]s`^[wpyjlonrXdXdYYOJPYD\T|mKVeZoLOOYbNPTGOLPӼjVo_>DrRWM;?YYDSN[WMD\{jķȺNA;3;C:2;MH7EOGj?筪B;MW;>S2-?G7?UMMUƸJkSMJa80/87=YYe_F9@B849l|IFdGɯE=V̾N?.(.GPMn`I;>JA<><9=fAG|H.;ʺPD2&(>^WjcJA<=EJ:3?[OjֽJ46]X;.+-4L~E49==>=UI;C\C69hǶM15W?/')9E/1?XNDOL:39෥O41JM5($-L_:04DUMKUH84@۱9-5J0%(7NսC117>Fb_?38MʩR/);̽`O.'+8Z׸A./67>r[44?hƥK**@d.(,;KoY7611:MjE9=O˪e0(0LA..4CRٹC9867:DVP?A`ֲ=(*;RSZb:36?@OɽWB?:55>XN>Etҿ.'/<<;ZO;;DD=QMOJ>67FRJDZŪ8'+54/9FF\OAWtGD_Y>9CfSI俺:4:<4/8A?>?NXN^XNJIOzĿhRMOYgcZUOKGHN\y\UV^mk]YUX]f~ticbm~noyvi_ZQLKLKIJMNP[yold\XSOKHIJIIIIKLQX`ysgbYWXTPPOORW[_fmrmihkotyolncfme_]bhcloqy{lhqj^\^aY\defch_]ak|ooa`WVQVXRcekcW]^Y^kozqa\[[XW^ecXXW\]d}ok[^XWT^a[aX]m^l}yIpQ_qaabX~_t{dkijU[l@N{OIP^vcYʾkIME;:835:1:L?IbvWF1//)),,.6<@CbbV}SǷE41,&)(&+/2>IT۽ĭD71)(+$(/+7D>gbH9/,+)'+--<=Fnʿ_I5/.+)+,-49;MPͿO;4/-++--29:DITftƿV:20+)+),01:>G_~ιA64,++(,..7:AR_߻J:4-+))**./7BKϸôL>5/,)))*-.4;FR_ú[?60,*)(**.17?KVȺlG94-+*(*+-16>GPZ@72-**)*+-2:@IóQ<7.,*(*(,.3=@Yȹi?:0,+&((*.1:?OȺeB=/.*')(+-18=YɶMB5/-()(*,.59AñbH9/.(((',-0:=vʲM;0.('''*,.8<^DzM92-('&(+,/6=ܿdA7/*(&((+-19NʵF;0+*'((*,/7IoзG;/,(&('),.9Vj\F8.,(')(*-/BURB2/,(*)),-:LOWG7/-**++,/;IXüQG50-+++--2FJǿ]A=/-,,*,/-5457>>ADGLVkhUMHDB@BEHLS]xi^XSONOOQVZ_irqifcaa_^b_afdglny|jea\\\Y\]]dlrrg`[YXUWXW]afyqgc^[\ZY[[[_bhq}wk`\YUUUUXZ^dl}ne]YVTTUVY\ck{{nc]ZWVUVWY^dolb^ZYXWXZ\_go||rjd`][ZXZ]`ho~~xsmkifgghmopy~wrokjigikknrqw}wspopmjkheecefeilnwog`[XWUSONOST[nlZQJE@><;:9;=BMbýnZPJDA>=;98767:>EOxƽyZOLJGFDB@=<:86569;840/04:Ei̽QGCCBCGJMNMMHA=83/./29EqɼpNFDEDEJNRTTRLC=83/--.4=P־bMFDEFGLRWXVRLB<73/--.1:Kø^MHEFFHLPQQNKE=9630/./3;MùkPHB??@AEILMKIE?<:876679AFJJIJIECCDEDDEEFJVyVMGA???@CINRTSTRNLKKIFDB@?@EO~ļ_OJD@?BEIMT_ic_^YOKJHC?=<;:;?Ilļ`VPLHGJMORYa]UPNJDA@><:8889<;=<:9:=?COڿOLOHA?F\dNYtP;156/-0B;17NpT[պ\3JG7A7<~cC9]\MP<--..7AEM~ȼĩ?=o>0~=9EN;+:l[PquG@<.,6:HyW .:]q=586:?4,D̸H59>76MoPbI<\~꼚U'1>>5-6>9/NC=?NNHxq<I7/@m`<>ھCA`[9//OD=_J@k8Y9/;O<1>w]W?GaG@US?2/NSPޱC6Jf=ˮX1.Bi?8H]IRPPPZNEFD?69IU_ٸN3>LƻHCF`oPn[OI?<9=OTR޷Q4:S=-5^U@FUͼJFKPOPqJ?><9;J[­;2@TҸZ8.=mIAKc»FAF[_IVnM=68??Kȳe/5迿>-1NWFHY˻O@CJXQQN=86=HI\ư]59sx?..HcFF\κK>?G[VNM=51>SPq>2=߽Y8-1INEOýC;?N_\]lF838ATƳl6:Z㾼?/,JI8>T^?1/9DAI׽K46?BEUui?7=EK𿦗@;GZYN޸:03>E@LӽG57=?DPuYA;<@Xή?EIIG]G2/9A?CZ<9;;>J\m\NF>;GܷTKI?]·E:8:978=JWUSQF>?QլI:;SúL87;;:DĿS=768:>I_wJ>AWٮP73C̾H:;>:7?aM>:989>NvLK[bs_91=F9;?=:>TJ?=<:;A\tpufy<3:WN=:87?ZdkiKA@I\rдB:>IQKC@A<88>GLHMltSKHMg{bֹWMUz^C968;98@NWX^[PVn`¾tXH@<>A@ENSYb^ba\_je]uoei]midsiffbOLOOKL]c}mpm~{cqT\xxmlh_dOWQNNNW`\[fm]`hYkkw_hea[PRYUQMXTKNX\i^_a]ZopU_fQGT]WORnw]\cskz\irutnphko\]pZUd^]d^zpaU]tuxro^VsV[aRMNU[_gwXhe\h`TYWl^eii_\ymi~w{fklY\ton{ox}|}bPT[cxu}~|u^d\^uxcpdchqumvyn_pkXf[_xm_zj[x_ogydkegm_pm}aj}t|uopgbme`^Ymp^|w{lwuocoaOeX^]`e^meifnxvsqomh|olpp|`[^ebqfy{lifzjXZen|fvvpkgmw]z_\qnpyp{gj\jcXX\bup^i]haJKmesS`u_\tQWYO]zmb]\|f]OJ=9C=_SOG14OF4@M66DκdQKC327JfOJgݪ\B9=ǭR21F96=kD?G959RNEEXި>9=ųL19O>>BݿM>@L>:U>AHG>=>D>97:M;2GG;WN=4?k?CXK?7:=;64B\¥H8<dzb9>KD:=mL?EF@86<=?=A_N7?ĸNavB=@TʱN;^JGIG>7>HTI<=>C=777?CDLf˾O>XɻP>;?<6:BXTC<=512H\WKLοM=X̿<9>L=66BUA609@=77IOFº<=\J<_wLZͽR:Fq@=AN>3/4??=:?MH?9AWon\»H>MyL==FI:208@@>;AEB=>LZmdɿCGNhD?ANE:329<<:GNfȽTOMTVFBDIF<759;;<=@?>>ENT_w½YWWhTHCBF@<87::;:=?=>@ILP[ozf[\MDA?A?=<9;;<<=>=?AGKO``ZLA?=AB??=>==>=>>>BDILWiĻ^^MDA>@DCC@??>=====>CFCD@?>:;:<=@BDDFE@=::::9;>?CGWȺqN@>==>@DDGC><:::;;>BIMTȽRHA?>?CDEGHGDCBA@????@EM]ǼcNEA@@@@BA@?>=;:::9:;>GWǺWHA>=>???@?>>=;::99:=>?@BCB@?=;:9878;?J_ԿfLC>==>?@AA@?=<:87668@AA@>=;976569>I^ѽdLB><<<=>@BB@><986457;BPnND><;;<>?@A@><9754469?NùnNC><<<<>??@?>=:865469?LŹnMC=;;;<>?ABA?>;975469=ItȺQE>;:<=>?AB@?=;976568?ABBA>;976557;BShMB=::;<=>@BA?=;97767;ANoͽZI?;99:;=?@BB@><::88:>FSʼWG>:89:;<>@BCB?=<<;;?@BB@>==<<=@HT}úyPD=:99:;>D????>=>??>@KZǼXJA;99;=<=@DDBBCDCAAEJP[ȾsYK@>>?>=>ABA@BECBBDFIMYyǽoXKA>>>>>?BAAACEDEGILNYnt[LEEEB??DDDDGLMJLR]hhniVPMIGFIKKLOUPNPWWV\_kxm]UOOOMNPQOORRSPOX^^ilvx~snn_XSSWXVSUNNOMPSQVYnzoa]Z]VU[P[MUZOa[akv[NTNQ]n{i^gfbei[f^^\at]cne\Zlrihkh`wghi^k^\c^TUSMmc[Zhd|g|^Zft^rcvZ^wxlyYYj_dthbhWg~l_vm`nvh^ZsqX[r`jqxefewofrh[`U{b[nY{Wtqdoyk|bgZj|yd\gllVpjcql}}wgknYvtl~nr^loqi]ncW`m^d|Wd[nhkgf}\jpgtVeUndmnwrj_`Ykl_m_f{dxgnl\[XdRkl`Vk]_eV]Yak\j_nZtVk_ZnX^P{ddmbvmm[]m]_RlU\tluOjgoY^l[KQypoogk\i_wiw[vY{gs[gqirlro{Xfde|cg`fvj^tjh}koo`znd{njwfhjvi~ns}rfztbqm_byi_zsmakq~~poln^[]`ina[ST^XRjľQB;61022359?GOs¿R:55,)*,//7K\hUPTGOWҬN2.-$!$',,8n;iC29;79ͫ4*+( &-18渳P5*',0-0FӺLA>4/2:@FWYF?90.1306@P[ûVE<78@GTpľeZC536205>IOOB8346:ADQUcO=885//48;Cj^OG?>DIFITZZe¸O=:5../48>\ƿ}TE<::;;;?HOsɿN??93039;?Lg[LKIFDAEFHM]ƾYC=:7427=A?>BJSOX¿eOJKHA@BGHEHNOIKXX[TZ_adiq{ma\^cONNLLHKLLMPX^TQW_m`h}n[g``]f~VfZWWMIKKJO\[US^s\hwuR~ZxgyVS\VI[NIMITKI[Xkk^]z]hwk[MNQQWN_HT[V{e\V\YinYq_`i]ZZNM_yOkml}TxoHkK]dm}wWi~r]Oexlkl`W[Zmq\_ve[ylb^\rp}dc_\ejV]]WZl|txmb_}k{Zjfm||pj]eoenqonjqvnm}b{lxmmxvoqiv~}i{~yzafxziv~nzllumqlqm|||uyqxumxvlqwo{vzklyo}t~vuwrnoqnihijnmowy|}~{vutttrsrqrpppqsruvv|}~~}}~}xzxvtuustvwzz~~}{wvtrrqqpooptvwz{~|zzyvvvtqqrtuvx{z}~}}}}||||}|}yyzy{z{|{|}}~||}~~}}}{}}{ywuuuuwwxy|}|}~~~~~~~~}~}~~}}zxyzyyy{z|~}||z{{ywyyy{~`wNEeSgl{LBfaXjWSWjǼ^FD>TFVPDm{inTTWQGUv[WqqwZ~PS\OVoI[O~`{[}U_cXIQXHEK^O=N\JMOnnսkN?=5-0/.2:EJn`SKIBJNVL=5/+(-./8KοG641//5AG[ºV;433,0<;>T˹^I4//./4>DJkŽI8798/:IBFahKG>32768DZWRuɿB95=;4FOJJ`\_N?F?8885GK=Qm`JJcOJSWBKI?;=FCIONHDOX̾IMMB;HUIYbG:AIEG^ZHLF=68>E<:OiUGALBCXG?lOf{L6/9><;Qd_<5=HC=J\LBAIMMl˽PKxAGGE3/:CE=JcL`O>=MSE?CLIJRnſY==l`C?BO@77=I@Eai]YJ>7:AGFDOeY;:Z[DALI739GICQeLIB=7:EVf[dƼY@DV>=E<62:DEEJmwXSROF?EQhgjyùN=G{K<=C?86>JLLW_MEGFEISmfÿ{OBNliL>>?>87=FMKUo|^O\b[MK\o{fgͿmnNIUzTA=>A=;=Jgj\UV[ZSPOUZ^gk¼__ozTG?@>=>BMSYTW[X[YZZW[Z_ektoo~¿rq}iOGEFGEGKQVTTU[YSW\_VOOTZVW^ŽycXRONJHIMRSSSX[[XQNKKLNQYg}ſn[SOLIJLNONNNNNNONNMOSVY\cgm{yx_XTOKJHIIHIHIIJKKJJMOSW[bfmx}m]VRMKIHHGEDDEFGIKLLMPUX\enx`VOLHFEEFFFEEFFFGKNRW[bmtxfZSOMKIGGGFFFGHIKMOSWZ^gq{x`VPMKJHHHHGGHIJJJMQV[_ivm\TNJGFEDCCDDEEGIKNRW^isdUMJFCAA@@A@ACDFHKNS[cnlYOJGDBBBCCCDEEFGILNRX`si[SMJHGGGGGHIIJKLNPUY_gp}|k]VOLJJJJJJJKLLMOQTX\bl{|rrv`XQMLKKJJKKLLMOSVX[_hp{xu{m^ZUOLKLLKKMNOOQUZ]ahs~ma[WTPNOOPOPRUVWY]bfksre_]YTSTUTTUWZ\\]bgghmw~}tkfb]ZYYXWWWYZZ\^_behjnt}~xvwz~zoe_^\[XXYYXYZ[]^_dhnu~}zxuuv{woh_\[\ZYXYZZZ[]`cdhnx|wwzvtyynje_\[[ZYZ[]]]_cimpx}{z~}oihea^]]]\\^_acdhmot}}}xuplhhgeb`abaabcfgggjmmmptvwzyvqmjhgfeeeeeggghjklmnnoqtwx||xsrqnljihgfggghhjlkkntwx{~|{|zyy}|{|xronnmmmnqsvxz|~}xvvvttuxz{|||~~}{yywwxuvwzz~|zxwvtsssttuwyy{}}~|{zyyxxyz{}~}zwwvtsuuvxyz{{}~|{{yz{{||~~|{yyyyxwxxyzy{}}|}|||{zzzzz{{|}~}}|zzz{{{}}|||||||{zzz{|}~~}~~}}~}|zyyz{|}}~}{zzzz{{|~~~~~~~~~}~}}~~~~~~~~~~~~}~~asterisk-testsuite-0.0.0+svn.5781/lib/python/asterisk/bridge_test_case.py000066400000000000000000000502471242304700500263510ustar00rootroot00000000000000#!/usr/bin/env python """ Copyright (C) 2012, Digium, Inc. Mark Michelson This program is free software, distributed under the terms of the GNU General Public License Version 2. """ import sys import logging import os from time import sleep sys.path.append("lib/python") from test_case import TestCase LOGGER = logging.getLogger(__name__) class BridgeTestCase(TestCase): """A test object for bridging tests""" ALICE_CONNECTED = '"Bob" <4321>' BOB_CONNECTED = '"Alice" <1234>' FEATURE_MAP = { 'blindxfer' : 1, 'atxfer' : 2, 'disconnect' : 3, 'automon' : 4, 'automixmon' : 5, 'parkcall' : 6, 'atxferthreeway' : 7, # other arbitrary DTMF features can be matched under DTMF code 8 # as "unknown" 'unknown' : 8 } def __init__(self, test_path='', test_config=None): """Class that handles tests involving two-party bridges. There are three Asterisk instances used for this test. 0 : the unit under test "UUT" 1 : the unit from which calls originate, also known as "Alice" 2 : the unit where calls terminate, also known as "Bob" """ TestCase.__init__(self, test_path) self.test_runs = [] self.current_run = 0 self.ami_uut = None self.ami_alice = None self.ami_bob = None self.call_end_observers = [] self.feature_start_observers = [] self.feature_end_observers = [] self.instances = 3 self.connections = 0 self.features = [] self.alice_channel = None self.bob_channel = None self.uut_alice_channel = None self.uut_bob_channel = None self.uut_bridge_id = None self.alice_hungup = False self.bob_hungup = False self.uut_alice_hungup = False self.uut_bob_hungup = False self.current_feature = 0 self.infeatures = False self.issue_hangups_on_bridged = False self.bridged = False self.audio_detected = False self.feature_success = False msg = ("BridgeTestCase hasn't raised the flag to indicate completion " "of all expected calls.") self.bridge_fail_token = self.create_fail_token(msg) if test_config is None: LOGGER.error("No configuration provided. Bailing.") raise Exception # Just a quick sanity check so we can die early if # the tests are badly misconfigured for test_run in test_config['test-runs']: if not 'originate_channel' in test_run: LOGGER.error("No configured originate channel in test run") raise Exception self.test_runs.append(test_run) if 'asterisk-instances' in test_config: self.instances = test_config['asterisk-instances'] self.create_asterisk(self.instances, "%s/configs/bridge" % os.getcwd()) LOGGER.info("Bridge test initialized") def run(self): """Override of TestCase.run""" TestCase.run(self) self.create_ami_factory(self.instances) def register_feature_start_observer(self, observer): """Register an observer to be notified when a feature is started Keyword Arguments: observer A callback function to be called when a feature occurs. The callback should take two arguments. This first will be this object; the second will be a dict object describing the feature that just occurred """ self.feature_start_observers.append(observer) def register_feature_end_observer(self, observer): """Register an observer to be notified when a feature is completed Keyword Arguments: observer A callback function to be called when a feature occurs. The callback should take two arguments. This first will be this object; the second will be a dict object describing the feature that just occurred """ self.feature_end_observers.append(observer) def ami_connect(self, ami): """AMI connect handler""" self.connections += 1 self.ast[ami.id].cli_exec("sip set debug on") self.ast[ami.id].cli_exec("iax2 set debug on") self.ast[ami.id].cli_exec("xmpp set debug on") if (ami.id == 0): self.ami_uut = ami self.ami_uut.registerEvent('Bridge', self.uut_bridge_callback) self.ami_uut.registerEvent('BridgeCreate', self.uut_bridge_create_callback) self.ami_uut.registerEvent('BridgeEnter', self.uut_bridge_enter_callback) self.ami_uut.registerEvent('BridgeLeave', self.uut_bridge_leave_callback) self.ami_uut.registerEvent('TestEvent', self.test_callback) self.ami_uut.registerEvent('Hangup', self.hangup_callback) LOGGER.info("UUT AMI connected") elif (ami.id == 1): self.ami_alice = ami self.ami_alice.registerEvent('UserEvent', self.user_callback) self.ami_alice.registerEvent('Hangup', self.hangup_callback) LOGGER.info("Alice AMI connected") elif (ami.id == 2): self.ami_bob = ami self.ami_bob.registerEvent('UserEvent', self.user_callback) self.ami_bob.registerEvent('Hangup', self.hangup_callback) LOGGER.info("Bob AMI connected") else: LOGGER.info("Unmanaged AMI ID %d received" % ami.id) if self.connections == self.instances: # We can get started with the test! LOGGER.info("Time to run tests!") self.run_tests() def run_tests(self): """Run the next test""" if self.current_run < len(self.test_runs): LOGGER.info("Executing test %d" % self.current_run) self.reset_timeout() self.uut_alice_channel = None self.uut_bob_channel = None self.start_test(self.test_runs[self.current_run]) elif self.current_run == len(self.test_runs): LOGGER.info("All calls executed, stopping") self.remove_fail_token(self.bridge_fail_token) self.set_passed(True) self.stop_reactor() def start_test(self, test_run): """Start a test run""" # Step 0: Set up event handlers and initialize values for this test run self.hangup = test_run['hangup'] if 'hangup' in test_run else None self.features = test_run['features'] if 'features' in test_run else [] self.alice_channel = None self.bob_channel = None self.uut_alice_channel = None self.uut_bob_channel = None self.uut_bridge_id = None self.alice_hungup = False self.bob_hungup = False self.uut_alice_hungup = False self.uut_bob_hungup = False self.current_feature = 0 self.infeatures = False self.issue_hangups_on_bridged = False self.bridged = False # Step 1: Initiate a call from Alice to Bob LOGGER.info("Originating call") self.ami_alice.originate(channel=test_run['originate_channel'], exten='test_call', context='default', priority='1', variable={'TALK_AUDIO': ('%s' % os.path.join(os.getcwd(), 'lib/python/asterisk/audio'))}) def user_callback(self, ami, event): """UserEvent AMI event callback""" if (event.get('userevent') == 'Connected'): if ami is self.ami_bob: self.bob_channel = event.get('channel') LOGGER.info("Bob's channel is %s" % self.bob_channel) elif ami is self.ami_alice: self.alice_channel = event.get('channel') LOGGER.info("Alice's channel is %s" % self.alice_channel) if (event.get('userevent') == 'TalkDetect'): if event.get('result') == 'pass': msg = "Two way audio properly detected between Bob and Alice" LOGGER.info(msg) self.audio_detected = True self.check_identities( alice_connected_line=BridgeTestCase.ALICE_CONNECTED, bob_connected_line=BridgeTestCase.BOB_CONNECTED, bob_bridge_peer=self.uut_alice_channel, alice_bridge_peer=self.uut_bob_channel) else: LOGGER.warning("Audio issues on bridged call") self.stop_reactor() return (ami, event) def hangup_callback(self, ami, event): """Hangup AMI event callback""" if ami is self.ami_bob and event.get('channel') == self.bob_channel: LOGGER.info("Bob has hung up") self.bob_hungup = True elif (ami is self.ami_alice and event.get('channel') == self.alice_channel): LOGGER.info("Alice has hung up") self.alice_hungup = True elif ami is self.ami_uut: if event.get('channel') == self.uut_alice_channel: LOGGER.info("UUT Alice hang up") self.uut_alice_hungup = True elif event.get('channel') == self.uut_bob_channel: LOGGER.info("UUT Bob hang up") self.uut_bob_hungup = True else: LOGGER.debug('Passing on Hangup for %s' % event['channel']) if (self.bob_hungup and self.alice_hungup and self.uut_alice_hungup and self.uut_bob_hungup): for callback in self.call_end_observers: callback(self.ami_uut, self.ami_alice, self.ami_bob) # Test call has concluded move on! self.current_run += 1 self.run_tests() return (ami, event) def uut_bridge_create_callback(self, ami, event): """BridgeCreate AMI event callback from UUT instance This event only applies to Asterisk 12 and later. """ LOGGER.debug('Bridge ID: %s' % event.get('bridgeuniqueid')) self.uut_bridge_id = event.get('bridgeuniqueid') return (ami, event) def uut_bridge_enter_callback(self, ami, event): """BridgeEnter AMI event callback from UUT instance This event only applies to Asterisk 12 and later. """ if self.uut_bridge_id != event.get('bridgeuniqueid'): return LOGGER.debug('Bridge ID: %s' % event.get('bridgeuniqueid')) channel = event.get('channel') if 'alice' in channel and self.uut_alice_channel is None: self.uut_alice_channel = channel LOGGER.info('UUT Alice Channel: %s' % self.uut_alice_channel) elif 'bob' in channel and self.uut_bob_channel is None: self.uut_bob_channel = channel LOGGER.info('UUT Bob Channel: %s' % self.uut_bob_channel) LOGGER.debug("Bridge is up between %s and %s" % (self.uut_alice_channel, self.uut_bob_channel)) LOGGER.debug("Type of bridge is %s" % event.get('bridgetype')) self.bridged = True if self.issue_hangups_on_bridged: self.send_hangup() return (ami, event) def uut_bridge_leave_callback(self, ami, event): """BridgeLeave AMI event callback from UUT instance This event only applies to Asterisk 12 and later. """ LOGGER.debug('Bridge ID: %s' % event.get('bridgeuniqueid')) LOGGER.debug("Bridge is down") self.bridged = False return (ami, event) def uut_bridge_callback(self, ami, event): """Bridge AMI event callback from UUT instance This event only applies to Asterisk 11 and earlier. """ if self.uut_alice_channel is None: self.uut_alice_channel = event.get('channel1') LOGGER.info('UUT Alice Channel: %s' % self.uut_alice_channel) if self.uut_bob_channel is None: self.uut_bob_channel = event.get('channel2') LOGGER.info('UUT Bob Channel: %s' % self.uut_bob_channel) if event.get('bridgestate') == 'Link': LOGGER.debug("Bridge is up between %s and %s" % (event.get('channel1'), event.get('channel2'))) LOGGER.debug("Type of bridge is %s" % event.get('bridgetype')) self.bridged = True if self.issue_hangups_on_bridged: self.send_hangup() else: LOGGER.debug("Bridge is down") self.bridged = False return (ami, event) def check_identities(self, alice_connected_line=None, bob_connected_line=None, alice_bridge_peer=None, bob_bridge_peer=None): """Check the identities of Alice & Bob Keyword Arguments: alice_connected_line The expected connected line value for Alice. Default is None bob_connected_line The expected connected line value for Bob. Default is None alice_bridge_peer The expected bridge peer for Alice. Default is None bob_bridge_peer The expected bridge peer for Bob. Default is None Note: setting any parameter to None will cause it to not be checked """ def alice_connected(value, expected): """Handle alice connected line""" LOGGER.info("Alice's Connected line is %s" % value) if value != expected: LOGGER.warning("Bad Connected line value for Alice: expected " "%s, actual %s" % (expected, value)) self.set_passed(False) def bob_connected(value, expected): """Handle bob connected line""" LOGGER.info("Bob's Connected line is %s" % value) if value != expected: LOGGER.warning("Bad Connected line value for Bob: expected %s, " "actual %s" % (expected, value)) self.set_passed(False) def alice_bridgepeer(value, expected): """Handle alice BRIDGEPEER variable""" LOGGER.info("Alice's BRIDGEPEER is %s" % value) if value != expected: LOGGER.warning("Bad BRIDGEPEER value for Alice: expected %s, " "actual %s" % (expected, value)) self.set_passed(False) def bob_bridgepeer(value, expected): """Handle bob BRIDGEPEER variable""" LOGGER.info("Bob's BRIDGEPEER is %s" % value) if value != expected: LOGGER.warning("Bad BRIDGEPEER value for Bob: expected %s, " "actual %s" % (expected, value)) self.set_passed(False) self.execute_features() if alice_connected_line is not None: self.ami_uut.getVar(self.uut_alice_channel, 'CONNECTEDLINE(all)').addCallback(alice_connected, alice_connected_line) if bob_connected_line is not None: self.ami_uut.getVar(self.uut_bob_channel, 'CONNECTEDLINE(all)').addCallback(bob_connected, bob_connected_line) if alice_bridge_peer is not None: self.ami_uut.getVar(self.uut_alice_channel, 'BRIDGEPEER').addCallback(alice_bridgepeer, alice_bridge_peer) if bob_bridge_peer is not None: self.ami_uut.getVar(self.uut_bob_channel, 'BRIDGEPEER').addCallback(bob_bridgepeer, bob_bridge_peer) def execute_features(self): """Execute the next feature for this test""" if self.current_feature < len(self.features): self.infeatures = True self.reset_timeout() LOGGER.info("Going to execute a feature") self.execute_feature(self.features[self.current_feature]) else: LOGGER.info("All features executed") self.ami_uut.userEvent("features_executed") self.send_hangup() def execute_feature(self, feature): """Execute the specified feature""" if (not 'who' in feature or not 'what' in feature or not 'success' in feature): LOGGER.warning("Missing necessary feature information") self.set_passed(False) if feature['who'] == 'alice': ami = self.ami_alice channel = self.alice_channel elif feature['who'] == 'bob': ami = self.ami_bob channel = self.bob_channel else: LOGGER.warning("Feature target must be 'alice' or 'bob'") self.set_passed(False) if feature['what'] not in BridgeTestCase.FEATURE_MAP: LOGGER.warning("Unknown feature requested") self.set_passed(False) if feature['success'] == 'true': self.feature_success = True else: self.feature_success = False for observer in self.feature_start_observers: observer(self, self.features[self.current_feature]) LOGGER.info("Sending feature %s from %s" % (feature['what'], feature['who'])) # make sure to put a gap between DTMF digits to ensure that events # headed to the UUT are not ignored because they occur too quickly sleep(0.25) ami.playDTMF(channel, BridgeTestCase.FEATURE_MAP[feature['what']]) sleep(0.25) if ((feature['what'] == 'blindxfer' or feature['what'] == 'atxfer') and 'exten' in feature): # playback the extension requested for digit in list(feature['exten']): sleep(0.25) ami.playDTMF(channel, digit) sleep(0.25) def test_callback(self, ami, event): """TestEvent AMI event callback""" if event.get('state') != 'FEATURE_DETECTION': return if not self.infeatures: # We don't care about features yet, so # just return return LOGGER.info("Got FEATURE_DETECTION event") if event.get('result') == 'success': LOGGER.info("Feature detected was %s" % event.get('feature')) if not self.feature_success: LOGGER.warning("Feature succeeded when failure expected") self.set_passed(False) elif (self.features[self.current_feature]['what'] != event.get('feature')): LOGGER.warning("Unexpected feature triggered") self.set_passed(False) else: LOGGER.info("No feature detected") if self.feature_success: LOGGER.warning("Feature failed when success was expected") self.set_passed(False) for observer in self.feature_end_observers: observer(self, self.features[self.current_feature]) # Move onto the next feature! self.current_feature += 1 self.execute_features() return (ami, event) def send_hangup(self): """Send hangup to the channel specified by self.hangup""" if not self.hangup: LOGGER.info("No hangup set. Hang up will happen externally") return if not self.bridged: LOGGER.info("Delaying hangup until call is re-bridged") self.issue_hangups_on_bridged = True return LOGGER.info("Sending a hangup to %s" % self.hangup) if self.hangup == 'alice': ami = self.ami_alice channel = self.alice_channel elif self.hangup == 'bob': ami = self.ami_bob channel = self.bob_channel else: raise Exception("Invalid hangup target specified: %s" % self.hangup) ami.hangup(channel) def register_call_end_observer(self, callback): """Register an observer to be called when a call ends The callback should take three parameters: (1) The AMI instance for the UUT (2) The AMI instance for Alice (3) The AMI instance for Bob """ self.call_end_observers.append(callback) asterisk-testsuite-0.0.0+svn.5781/lib/python/asterisk/buildoptions.py000066400000000000000000000065451242304700500256000ustar00rootroot00000000000000#!/usr/bin/env python """Asterisk Build Options Handling This module implements an Asterisk build options parser. It tracks what build options Asterisk was compiled with, and returns whether or not a particular build option was enabled. Copyright (C) 2011-2012, Digium, Inc. Matt Jordan This program is free software, distributed under the terms of the GNU General Public License Version 2. """ import sys import unittest class AsteriskBuildOptions(object): """Tracks the build options for an instance of Asterisk""" def __init__(self, path=None): """Construct an instance of AsteriskBuildOptions Keyword Arguments: path The path to locate the buildopts.h file under """ self._flags = {} buildopts_hdr_paths = [ "../include/asterisk/buildopts.h", "/usr/include/asterisk/buildopts.h", "/usr/local/include/asterisk/buildopts.h" ] if path: buildopts_hdr_paths.insert(0, path) for hdr_path in buildopts_hdr_paths: if (self.__parse_buildopts_file(hdr_path)): return raise Exception("Failed to open any build options files") def __parse_buildopts_file(self, path): """Extract and parse the build options""" ret_val = False file_lines = [] try: with open(path, "r") as build_opt_file: file_lines = build_opt_file.readlines() except IOError: return ret_val except: print "Unexpected error: %s" % sys.exc_info()[0] return ret_val for line in file_lines: if "#define" in line: define_tuple = line.partition(' ')[2].partition(' ') if (define_tuple[0] == "" or define_tuple[2] == ""): msg = ("Unable to parse build option line [%s] into " "compiler flag token and value" % line) print msg else: flag = define_tuple[0].strip() allowed = define_tuple[2].strip() self._flags[flag] = allowed ret_val = True return ret_val def check_option(self, build_option, expected_value="1"): """ Checks if a build option has been set to an expected value Keyword Arguments: build_option The Asterisk build option set in buildopts.h to check for expected_value The value the option should have. Default is 1. Note: if the buildOption's expectedValue is "0" (for not defined), then this method will return True if either the buildOption does not exist OR if it exists and matches. Returns: True if the build option exists and the expected value matches; false otherwise """ if build_option in self._flags.keys(): return expected_value == self._flags[build_option] elif expected_value == "0": return True return False class AsteriskBuildOptionsTests(unittest.TestCase): """Unit tests for AsteriskBuildOptions""" def test_1(self): """Test the defaults paths""" build_options = AsteriskBuildOptions() self.assertTrue(1) def main(): """Main entry point for unit tests""" unittest.main() if __name__ == "__main__": main() asterisk-testsuite-0.0.0+svn.5781/lib/python/asterisk/cdr.py000066400000000000000000000135571242304700500236360ustar00rootroot00000000000000#!/usr/bin/env python """Asterisk call detail record testing This module implements an Asterisk CDR parser. Copyright (C) 2010, Digium, Inc. Terry Wilson This program is free software, distributed under the terms of the GNU General Public License Version 2. """ import unittest import sys import astcsv import logging LOGGER = logging.getLogger(__name__) class CDRModule(object): """A module that checks a test for expected CDR results""" def __init__(self, module_config, test_object): """Constructor Parameters: module_config The yaml loaded configuration for the CDR Module test_object A concrete implementation of TestClass """ self.test_object = test_object # Build our expected CDR records self.cdr_records = {} for record in module_config: file_name = record['file'] ast_id = record.get('id') or 0 if ast_id not in self.cdr_records: self.cdr_records[ast_id] = {} if file_name not in self.cdr_records[ast_id]: self.cdr_records[ast_id][file_name] = [] for csv_line in record['lines']: # Set the record to the default fields, then update with what # was passed in to us dict_record = dict((k, None) for k in AsteriskCSVCDRLine.fields) if csv_line is not None: dict_record.update(csv_line) self.cdr_records[ast_id][file_name].append( AsteriskCSVCDRLine(**dict_record)) # Hook ourselves onto the test object test_object.register_stop_observer(self._check_cdr_records) def _check_cdr_records(self, callback_param): """A deferred callback method that is called by the TestCase derived object when all Asterisk instances have stopped Parameters: callback_param """ LOGGER.debug("Checking CDR records...") try: self.match_cdrs() except: LOGGER.error("Exception while checking CDRs: %s" % sys.exc_info()[0]) return callback_param def match_cdrs(self): """Called when all instances of Asterisk have exited. Derived classes can override this to provide their own behavior for CDR matching. """ expectations_met = True for ast_id in self.cdr_records: ast_instance = self.test_object.ast[ast_id] for file_name in self.cdr_records[ast_id]: records = self.cdr_records[ast_id][file_name] cdr_expect = AsteriskCSVCDR(records=records) cdr_file = AsteriskCSVCDR(filename="%s/%s/cdr-csv/%s.csv" % (ast_instance.base, ast_instance.directories['astlogdir'], file_name)) if cdr_expect.match(cdr_file): LOGGER.debug("%s.csv: CDR results met expectations" % file_name) else: LOGGER.error("%s.csv: actual did not match expected." % file_name) expectations_met = False self.test_object.set_passed(expectations_met) class AsteriskCSVCDRLine(astcsv.AsteriskCSVLine): """A single Asterisk call detail record""" fields = ['accountcode', 'source', 'destination', 'dcontext', 'callerid', 'channel', 'dchannel', 'lastapp', 'lastarg', 'start', 'answer', 'end', 'duration', 'billsec', 'disposition', 'amaflags', 'uniqueid', 'userfield'] def __init__(self, accountcode=None, source=None, destination=None, dcontext=None, callerid=None, channel=None, dchannel=None, lastapp=None, lastarg=None, start=None, answer=None, end=None, duration=None, billsec=None, disposition=None, amaflags=None, uniqueid=None, userfield=None): """Construct an Asterisk CSV CDR. The arguments list definition must be in the same order that the arguments appear in the CSV file. They can, of course, be passed to __init__ in any order. AsteriskCSVCDR will pass the arguments via a **dict. """ astcsv.AsteriskCSVLine.__init__(self, AsteriskCSVCDRLine.fields, accountcode=accountcode, source=source, destination=destination, dcontext=dcontext, callerid=callerid, channel=channel, dchannel=dchannel, lastapp=lastapp, lastarg=lastarg, start=start, answer=answer, end=end, duration=duration, billsec=billsec, disposition=disposition, amaflags=amaflags, uniqueid=uniqueid, userfield=userfield) class AsteriskCSVCDR(astcsv.AsteriskCSV): """A representation of an Asterisk CSV CDR file""" def __init__(self, filename=None, records=None): """Initialize CDR records from an Asterisk cdr-csv file""" astcsv.AsteriskCSV.__init__(self, filename, records, AsteriskCSVCDRLine.fields, AsteriskCSVCDRLine) class AsteriskCSVCDRTests(unittest.TestCase): """Unit tests for AsteriskCSVCDR""" def test_cdr(self): """Test the self_test/Master.csv record""" cdr = AsteriskCSVCDR("self_test/Master.csv") self.assertEqual(len(cdr), 2) self.assertTrue(AsteriskCSVCDRLine(duration=7, lastapp="hangup").match(cdr[0], exact=(True, True))) self.assertTrue(cdr[0].match(AsteriskCSVCDRLine(duration=7, lastapp="hangup"), exact=(True, True))) self.assertFalse(cdr[1].match(cdr[0])) self.assertFalse(cdr[0].match(cdr[1])) self.assertEqual(cdr[0].billsec, "7") self.assertTrue(cdr.match(cdr)) cdr2 = AsteriskCSVCDR("self_test/Master2.csv") self.assertFalse(cdr.match(cdr2)) if __name__ == '__main__': unittest.main() # vim:sw=4:ts=4:expandtab:textwidth=79 asterisk-testsuite-0.0.0+svn.5781/lib/python/asterisk/cel.py000066400000000000000000000166741242304700500236340ustar00rootroot00000000000000#!/usr/bin/env python """Asterisk call detail record testing This module implements an Asterisk CEL parser. Copyright (C) 2010, Digium, Inc. Terry Wilson This program is free software, distributed under the terms of the GNU General Public License Version 2. """ import yaml import unittest import astcsv import re import logging LOGGER = logging.getLogger(__name__) class CELSniffer(object): """A pluggable module that sniffs AMI CEL events and dumps them into a YAML file. Useful during test writing to create a baseline of expected CEL events """ def __init__(self, module_config, test_object): """Constructor""" test_object.register_ami_observer(self.ami_connect) test_object.register_stop_observer(self.stop_handler) self.events = [] if module_config is None: self.filters = {} self.display = [] else: self.filters = module_config.get('filters') or {} self.display = module_config.get('display') or [] def ami_connect(self, ami): """AMI connection handler""" ami.registerEvent('CEL', self.cel_handler) def cel_handler(self, ami, event): """Handle a CEL event""" for filter_key, filter_value in self.filters.items(): if re.match(filter_value, event.get(filter_key.lower())) is None: return self.events.append(event) return (ami, event) def stop_handler(self, reason): """Write out the file. Currently hard codd to cel_events.yaml""" stream = file('cel_events.yaml', 'w') if len(self.display) == 0: yaml.dump(self.events, stream) else: items = [] for ev_item in self.events: item = {} for key in self.display: key = key.lower() if key not in ev_item: continue item[key] = ev_item[key] items.append(item) yaml.dump(items, stream) stream.close() return reason class CELModule(object): """Class that checks a test for expected CEL results""" def __init__(self, module_config, test_object): """Constructor Parameters: module_config The yaml loaded configuration for the CEL Module test_object A concrete implementation of TestClass """ self.test_object = test_object # Build our expected CEL records self.cel_records = {} for record in module_config: file_name = record['file'] if file_name not in self.cel_records: self.cel_records[file_name] = [] for csv_line in record['lines']: # Set the record to the default fields, then update with what # was passed in to us dict_record = dict((k, None) for k in AsteriskCSVCELLine.fields) if csv_line is not None: dict_record.update(csv_line) file_records = self.cel_records[file_name] file_records.append(AsteriskCSVCELLine(**dict_record)) # Hook ourselves onto the test object test_object.register_stop_observer(self._check_cel_records) def _check_cel_records(self, callback_param): """A deferred callback method that is called by the TestCase derived object when all Asterisk instances have stopped Parameters: callback_param """ LOGGER.debug("Checking CEL records...") self.match_cels() return callback_param def match_cels(self): """Called when all instances of Asterisk have exited. Derived classes can override this to provide their own behavior for CEL matching. """ expectations_met = True for key in self.cel_records: cel_expect = AsteriskCSVCEL(records=self.cel_records[key]) cel_file = AsteriskCSVCEL(filename="%s/%s/cel-custom/%s.csv" % (self.test_object.ast[0].base, self.test_object.ast[0].directories['astlogdir'], key)) if cel_expect.match(cel_file): LOGGER.debug("%s.csv - CEL results met expectations" % key) else: msg = ("%s.csv - CEL results did not meet expectations. " "Test Failed." % key) LOGGER.error(msg) expectations_met = False self.test_object.set_passed(expectations_met) class AsteriskCSVCELLine(astcsv.AsteriskCSVLine): "A single Asterisk Call Event Log record" fields = ['eventtype', 'eventtime', 'cidname', 'cidnum', 'ani', 'rdnis', 'dnid', 'exten', 'context', 'channel', 'app', 'appdata', 'amaflags', 'accountcode', 'uniqueid', 'linkedid', 'bridgepeer', 'userfield', 'userdeftype', 'eventextra'] def __init__(self, eventtype=None, eventtime=None, cidname=None, cidnum=None, ani=None, rdnis=None, dnid=None, exten=None, context=None, channel=None, app=None, appdata=None, amaflags=None, accountcode=None, uniqueid=None, linkedid=None, bridgepeer=None, userfield=None, userdeftype=None, eventextra=None): """Construct an Asterisk CSV CEL. The arguments list definition must be in the same order that the arguments appear in the CSV file. They can, of course, be passed to __init__ in any order. AsteriskCSVCEL will pass the arguments via a **dict. """ astcsv.AsteriskCSVLine.__init__(self, AsteriskCSVCELLine.fields, eventtype=eventtype, eventtime=eventtime, cidname=cidname, cidnum=cidnum, ani=ani, rdnis=rdnis, dnid=dnid, exten=exten, context=context, channel=channel, app=app, appdata=appdata, amaflags=amaflags, accountcode=accountcode, uniqueid=uniqueid, linkedid=linkedid, bridgepeer=bridgepeer, userfield=userfield, userdeftype=userdeftype, eventextra=eventextra) class AsteriskCSVCEL(astcsv.AsteriskCSV): """A representation of an Asterisk CSV CEL file""" def __init__(self, filename=None, records=None): """Initialize CEL records from an Asterisk cel-csv file""" astcsv.AsteriskCSV.__init__(self, filename, records, AsteriskCSVCELLine.fields, AsteriskCSVCELLine) class AsteriskCSVCELTests(unittest.TestCase): """Unit tests for AsteriskCSVCEL""" def test_cel(self): """Test CEL using self_test/CELMaster1.csv""" cel = AsteriskCSVCEL("self_test/CELMaster1.csv") self.assertEqual(len(cel), 16) self.assertTrue(AsteriskCSVCELLine(eventtype="LINKEDID_END", channel="TinCan/string").match(cel[-1], silent=True, exact=(True, True))) self.assertTrue(cel[-1].match(AsteriskCSVCELLine( eventtype="LINKEDID_END", channel="TinCan/string"), silent=True, exact=(True, True))) self.assertFalse(cel[1].match(cel[0], silent=True)) self.assertFalse(cel[0].match(cel[1], silent=True)) self.assertEqual(cel[-1].channel, "TinCan/string") self.assertTrue(cel.match(cel)) cel2 = AsteriskCSVCEL("self_test/CELMaster2.csv") self.assertFalse(cel.match(cel2)) if __name__ == '__main__': logging.basicConfig() unittest.main() # vim:sw=4:ts=4:expandtab:textwidth=79 asterisk-testsuite-0.0.0+svn.5781/lib/python/asterisk/channel_test_condition.py000066400000000000000000000053661242304700500276020ustar00rootroot00000000000000#!/usr/bin/env python """Test condition for channels Copyright (C) 2011-2012, Digium, Inc. Matt Jordan This program is free software, distributed under the terms of the GNU General Public License Version 2. """ from twisted.internet import defer from test_conditions import TestCondition class ChannelTestCondition(TestCondition): """Test condition that checks for the existence of channels. If channels are detected and the number of active channels is greater than the configured amount, an error is raised. By default, the number of allowed active channels is 0. """ def __init__(self, test_config): """Constructor Keyword Arguments: test_config The TestConfig object for the test """ super(ChannelTestCondition, self).__init__(test_config) self.allowed_channels = 0 if ('allowedchannels' in test_config.config): self.allowed_channels = test_config.config['allowedchannels'] def evaluate(self, related_test_condition=None): """Evaluate this test condition Keyword Argument: related_test_condition The test condition that this condition is related to Returns: A deferred that will be called when evaluation is complete """ def __channel_callback(result): """Callback called from core show channels""" channel_tokens = result.output.strip().split('\n') active_channels = 0 for token in channel_tokens: if 'active channels' in token: active_channel_tokens = token.partition(' ') active_channels = int(active_channel_tokens[0].strip()) if active_channels > self.allowed_channels: msg = ("Detected number of active channels %d is greater than " "the allowed %d on Asterisk %s" % (active_channels, self.allowed_channels, result.host)) super(ChannelTestCondition, self).fail_check(msg) return result def _raise_finished(result, finish_deferred): """Raise the deferred callback""" finish_deferred.callback(self) return result finish_deferred = defer.Deferred() # Set to pass and let a failure override super(ChannelTestCondition, self).pass_check() exec_list = [ast.cli_exec('core show channels').addCallback(__channel_callback) for ast in self.ast] defer.DeferredList(exec_list).addCallback(_raise_finished, finish_deferred) return finish_deferred asterisk-testsuite-0.0.0+svn.5781/lib/python/asterisk/confbridge.py000066400000000000000000000221501242304700500251550ustar00rootroot00000000000000#!/usr/bin/env python # vim: sw=3 et: """Module that provides classes to track a ConfBridge test. This module has been superceded by the apptest module. Copyright (C) 2011, Digium, Inc. Matt Jordan This program is free software, distributed under the terms of the GNU General Public License Version 2. """ import sys import logging from test_state import TestState from twisted.internet import reactor sys.path.append("lib/python") LOGGER = logging.getLogger(__name__) class ConfbridgeChannelObject(object): """A tracking object that ties together information about the channels involved in a ConfBridge """ def __init__(self, conf_bridge_channel, caller_channel, caller_ami, profile_option=""): """Constructor Keyword Arguments: conf_bridge_channel The channel inside the Asterisk instance hosting the ConfBridge caller_channel The channel inside the Asterisk instance that called the ConfBridge server caller_ami An AMI connection back to the calling Asterisk instance profile_option Some string field that identifies the profile set for the conf_bridge_channel """ self.conf_bridge_channel = conf_bridge_channel self.caller_channel = caller_channel self.caller_ami = caller_ami self.profile = profile_option class ConfbridgeTestState(TestState): """Base class test state for ConfBridge. Allows states to send DTMF tones, audio files, hangup, and otherwise interact with the conference. As this inherits from test_state, this is also an entry in the state engine, such that it will receive test event notifications. Derived classes should handle these state notifications, and use the methods in this class to respond accordingly. """ def __init__(self, controller, test_case, calls=None): """Constructor Keyword Arguments: controller The TestStateController managing the test test_case The main test object calls A dictionary (keyed by conf_bridge_channel ID) of ConfbridgeChannelObjects """ TestState.__init__(self, controller) self.test_case = test_case self.calls = calls or {} self.__previous_dtmf = {} self.__previous_audio = {} if (len(self.calls) > 0): for key in self.calls: self.__previous_dtmf[key] = "" self.__previous_audio[key] = "" LOGGER.debug(" Entering state [" + self.get_state_name() + "]") def register_new_caller(self, channel_object): """Register a new ConfbridgeChannelObject with the state engine Keyword Arguments: channel_object An object that ties all of the various channels/AMI information together """ if not (channel_object.conf_bridge_channel in self.calls): self.calls[channel_object.conf_bridge_channel] = channel_object self.__previous_dtmf[channel_object.conf_bridge_channel] = "" self.__previous_audio[channel_object.conf_bridge_channel] = "" def get_state_name(self): """Should be overriden by derived classes and return the name of the current state """ pass def handle_default_state(self, event): """Can be called by derived classes to output a state that is being ignored """ msg = ("State [" + self.get_state_name() + "] - ignoring state change " + event['state']) LOGGER.debug(msg) def _handle_redirect_failure(self, reason): """Handle a redirect failure from Asterisk""" LOGGER.warn("Error sending redirect - test may or may not fail:") LOGGER.warn(reason.getTraceback()) return reason def hangup(self, call_id): """Hangs up the current call Keyword Arguments: call_id The channel name """ if call_id in self.calls: chan = self.calls[call_id].caller_channel ami = self.calls[call_id].caller_ami deferred = ami.redirect(chan, "caller", "hangup", 1) deferred.addErrback(self._handle_redirect_failure) else: LOGGER.warn("Unknown call ID %s" % call_id) def send_dtmf(self, call_id, dtmf): """Send a DTMF signal to the server Keyword Arguments: call_id The channel name, from the perspective of the ConfBridge app dtmf The DTMF code to send """ if call_id in self.calls: LOGGER.debug("Attempting to send DTMF %s via %s" % (dtmf, call_id)) ami = self.calls[call_id].caller_ami if (self.__previous_dtmf[call_id] != dtmf): ami.setVar(channel=self.calls[call_id].caller_channel, variable="DTMF_TO_SEND", value=dtmf) self.__previous_dtmf[call_id] = dtmf # Redirect to the DTMF extension - note that we assume that we only # have one channel to the other asterisk instance deferred = ami.redirect(self.calls[call_id].caller_channel, "caller", "sendDTMF", 1) deferred.addErrback(self._handle_redirect_failure) else: LOGGER.warn("Unknown call ID %s" % call_id) def send_sound_file(self, call_id, audio_file): """Send a sound file to the server Keyword Arguments: call_id The channel name, from the perspective of the ConfBridge app audio_file The local path to the file to stream """ if call_id in self.calls: LOGGER.debug("Attempting to send audio file %s via %s" % (audio_file, call_id)) ami = self.calls[call_id].caller_ami if (self.__previous_audio[call_id] != audio_file): ami.setVar(channel=self.calls[call_id].caller_channel, variable="TALK_AUDIO", value=audio_file) self.__previous_audio[call_id] = audio_file # Redirect to the send sound file extension - note that we assume # that we only have one channel to the other asterisk instance deferred = ami.redirect(self.calls[call_id].caller_channel, "caller", "sendAudio", 1) deferred.addErrback(self._handle_redirect_failure) else: LOGGER.warn("Unknown call ID %s" % call_id) def send_sound_file_with_dtmf(self, call_id, audio_file, dtmf): """Send a sound file to the server, then send a DTMF signal Keyword Arguments: call_id The channel name, from the perspective of the ConfBridge app audio_file The local path to the file to stream dtmf The DTMF signal to send Note that this is necessary so that when the audio file is finished, we close the audio recording cleanly; otherwise, Asterisk may detect the end of file as a hangup """ if call_id in self.calls: msg = ("Attempting to send audio file %s followed by %s via %s" % (audio_file, dtmf, call_id)) LOGGER.debug(msg) ami = self.calls[call_id].caller_ami if (self.__previous_audio[call_id] != audio_file): ami.setVar(channel=self.calls[call_id].caller_channel, variable="TALK_AUDIO", value=audio_file) self.__previous_audio[call_id] = audio_file if (self.__previous_dtmf[call_id] != dtmf): ami.setVar(channel=self.calls[call_id].caller_channel, variable="DTMF_TO_SEND", value=dtmf) self.__previous_dtmf[call_id] = dtmf # Redirect to the send sound file extension - note that we assume # that we only have one channel to the other asterisk instance deferred = ami.redirect(self.calls[call_id].caller_channel, "caller", "sendAudioWithDTMF", 1) deferred.addErrback(self._handle_redirect_failure) else: LOGGER.warn("Unknown call ID %s" % call_id) def schedule_dtmf(self, delay, call_id, dtmf): """Schedule and send a DTMF tone sometime later Keyword Arguments: delay The number of seconds to wait call_id The channel name, from the perspective of the ConfBridge app dtmf The DTMF code to send """ reactor.callLater(delay, self.send_dtmf, call_id, dtmf) def schedule_sound_file(self, delay, call_id, audio_file): """Schedule and send an audio file Keyword Arguments: delay The number of seconds to wait call_id The channel name, from the perspective of the ConfBridge app audio_file The local path to the file to stream """ reactor.callLater(delay, self.send_sound_file, call_id, audio_file) asterisk-testsuite-0.0.0+svn.5781/lib/python/asterisk/config.py000077500000000000000000000155651242304700500243370ustar00rootroot00000000000000#!/usr/bin/env python """Asterisk Configuration File Handling. This module implements interfaces for dealing with Asterisk configuration files. Copyright (C) 2010, Digium, Inc. Russell Bryant This program is free software, distributed under the terms of the GNU General Public License Version 2. """ # # TODO # - Implement config section template handling # import sys import re import unittest import logging LOGGER = logging.getLogger(__name__) def is_blank_line(line): """Is this a blank line?""" return re.match("\s*(?:;.*)?$", line) is not None class Category(object): """A category in an Asterisk configuration file. This is a helper class used along with ConfigFile. A category is section of an Asterisk configuration that will contain zero or more key/value pairs of options. """ def __init__(self, name, template=False): self.options = [] self.name = name self.template = template self.varval_re = re.compile(""" \s* # Leading whitespace (?P[\w|,\.-]+) # Option name \s*=>?\s* # Separator, = or => (?P[\w\s=_()/@|,'"\.<>:-]*) # Option value (can be zero-length) (?:;.*)?$ # Optional comment before end of line """, re.VERBOSE) def parse_line(self, line): """Parse a line in a category""" match = self.varval_re.match(line) if match is None: if not is_blank_line(line): LOGGER.warn("Invalid line: '%s'" % line.strip()) return self.options.append((match.group("name"), match.group("value").strip())) class ConfigFile(object): """An Asterisk Configuration File. Parse an Asterisk configuration file. """ def __init__(self, filename, config_str=None): """Construct an Asterisk configuration file object The ConfigFile object will parse an Asterisk configuration file into a python data structure. """ self.categories = [] self.category_re = re.compile(""" \s* # Leading Whitespace \[(?P[\w,\.-]+)\] # Category name in square brackets (?:\((?P