{"id":13758512,"url":"https://github.com/landro/TesTcl","last_synced_at":"2025-05-10T08:30:31.867Z","repository":{"id":2465590,"uuid":"3437860","full_name":"landro/TesTcl","owner":"landro","description":"when you don't have the balls to test your F5 BIG-IP iRules directly in production","archived":false,"fork":false,"pushed_at":"2023-11-01T18:13:37.000Z","size":414,"stargazers_count":98,"open_issues_count":17,"forks_count":30,"subscribers_count":19,"default_branch":"master","last_synced_at":"2024-11-16T15:37:41.849Z","etag":null,"topics":["big-ip","f5-bigip","irule","tcl","test-driven-development","testing-tools"],"latest_commit_sha":null,"homepage":"https://testcl.com","language":"Tcl","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/landro.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2012-02-14T07:26:51.000Z","updated_at":"2024-03-20T12:41:16.000Z","dependencies_parsed_at":"2024-08-03T13:11:40.173Z","dependency_job_id":null,"html_url":"https://github.com/landro/TesTcl","commit_stats":null,"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/landro%2FTesTcl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/landro%2FTesTcl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/landro%2FTesTcl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/landro%2FTesTcl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/landro","download_url":"https://codeload.github.com/landro/TesTcl/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253389431,"owners_count":21900760,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["big-ip","f5-bigip","irule","tcl","test-driven-development","testing-tools"],"created_at":"2024-08-03T13:00:31.581Z","updated_at":"2025-05-10T08:30:31.479Z","avatar_url":"https://github.com/landro.png","language":"Tcl","funding_links":[],"categories":["DevOps / CICD"],"sub_categories":[],"readme":"# Introduction\n\n**TesTcl** is a [Tcl](http://en.wikipedia.org/wiki/Tcl) library for unit testing\n[iRules](https://devcentral.f5.com/HotTopics/iRules/tabid/1082202/Default.aspx) which \nare used when configuring [F5 BIG-IP](http://www.f5.com/products/big-ip/) devices.\n\n## News\n- 4th May 2020 - Version [1.0.14](https://github.com/landro/TesTcl/releases) released\n- 10th November 2018 - Version [1.0.13](https://github.com/landro/TesTcl/releases) released\n- 26th September 2018 - Version [1.0.12](https://github.com/landro/TesTcl/releases) released\n- 24th May 2018 - Version [1.0.11](https://github.com/landro/TesTcl/releases) released\n- 23rd March 2017 - Version [1.0.10](https://github.com/landro/TesTcl/releases) released\n- 29th April 2016 - Version [1.0.9](https://github.com/landro/TesTcl/releases) released\n\n## Getting started\n\nIf you're familiar with unit testing and [mocking](http://en.wikipedia.org/wiki/Mock_object) in particular,\nusing TesTcl should't be to hard. Check out the examples below:\n\n### Simple example\n\nLet's say you want to test the following simple iRule found in *simple_irule.tcl*:\n\n```tcl\nrule simple {\n\n  when HTTP_REQUEST {\n    if { [HTTP::uri] starts_with \"/foo\" } {\n      pool foo\n    } else {\n      pool bar\n    }\n  }\n\n  when HTTP_RESPONSE {\n    HTTP::header remove \"Vary\"\n    HTTP::header insert Vary \"Accept-Encoding\"\n  }\n\n}\n```\n\nNow, create a file called *test_simple_irule.tcl* containing the following lines:\n\n```tcl\npackage require -exact testcl 1.0.14\nnamespace import ::testcl::*\n\n# Comment in to enable logging\n#log::lvSuppressLE info 0\n\nit \"should handle request using pool bar\" {\n  event HTTP_REQUEST\n  on HTTP::uri return \"/bar\"\n  endstate pool bar\n  run simple_irule.tcl simple\n}\n\nit \"should handle request using pool foo\" {\n  event HTTP_REQUEST\n  on HTTP::uri return \"/foo/admin\"\n  endstate pool foo\n  run simple_irule.tcl simple\n}\n\nit \"should replace existing Vary http response headers with Accept-Encoding value\" {\n  event HTTP_RESPONSE\n  verify \"there should be only one Vary header\" 1 == {HTTP::header count vary}\n  verify \"there should be Accept-Encoding value in Vary header\" \"Accept-Encoding\" eq {HTTP::header Vary}\n  HTTP::header insert Vary \"dummy value\"\n  HTTP::header insert Vary \"another dummy value\"\n  run simple_irule.tcl simple\n}\n```\n\n#### Installing JTcl including jtcl-irule extensions\n\n##### Install JTcl\nDownload [JTcl](https://jtcl-project.github.io/jtcl/), unzip it and add it to your path.\n\n##### Add jtcl-irule to your JTcl installation\nAdd the [jtcl-irule](http://landro.github.io/jtcl-irule/) extension to JTcl. If you don't have the time to build it yourself, you can download the \njar artifact from the [release v 0.9](https://github.com/landro/jtcl-irule/releases/tag/v0.9) page or you can use the direct [link](https://github.com/landro/jtcl-irule/releases/download/v0.9/jtcl-irule-0.9.jar).\nNext, copy the jar file into the directory where you installed JTcl.\nAdd jtcl-irule to the classpath in _jtcl_ or _jtcl.bat_.\n**IMPORTANT!** Make sure you place the _jtcl-irule-0.9.jar_ on the classpath **before** the standard jtcl-\u003cversion\u003e.jar\n\n###### MacOS X and Linux\n\nOn MacOs X and Linux, this can be achieved by putting the following line just above the last line in the jtcl shell script\n\n    export CLASSPATH=$dir/jtcl-irule-0.9.jar:$CLASSPATH\n    \n###### Windows\n\nOn Windows, modify the following line in jtcl.bat from \n\n    set cp=\"%dir%\\jtcl-%jtclver%.jar;%CLASSPATH%\"\n\nto\n\n    set cp=\"%dir%\\jtcl-irule-0.9.jar;%dir%\\jtcl-%jtclver%.jar;%CLASSPATH%\"\n\n##### Verify installation\n\nCreate a script file named *test_jtcl_irule.tcl* containing the following lines \n\n```tcl\nif {\"aa\" starts_with \"a\"} {\n  puts \"The jtcl-irule extension has successfully been installed\"\n}\n```\n\nand execute it using \n\n    jtcl test_jtcl_irule.tcl\n\nYou should get a success message. \nIf you get a message saying *syntax error in expression \"\"aa\" starts_with \"a\"\": variable references require preceding $*, jtcl-irule is not on the classpath **before** the standard jtcl-\u003cversion\u003e.jar. Please review instructions above.\n\n##### Add the testcl library to your library path\nDownload latest [TesTcl distribution](https://github.com/landro/TesTcl/releases) from github containing all the files (including examples) found in the project.\nUnzip, and add unzipped directory to the [TCLLIBPATH](http://jtcl.kenai.com/gettingstarted.html) environment variable:\n\nOn MacOS X and Linux:\n\n    export TCLLIBPATH=whereever/TesTcl-1.0.14\n    \nOn Windows, create a System Variable named `TCLLIBPATH` and make sure that the path uses forward slashes '/'\n\nIn order to run this example, type in the following at the command-line:\n\n    \u003ejtcl test_simple_irule.tcl\n\nThis should give you the following output:\n\n    **************************************************************************\n    * it should handle request using pool bar\n    **************************************************************************\n    -\u003e Test ok\n\n    **************************************************************************\n    * it should handle request using pool foo\n    **************************************************************************\n    -\u003e Test ok\n\n    **************************************************************************\n    * it should replace existing Vary http response headers with Accept-Encoding value\n    **************************************************************************\n    verification of 'there should be only one Vary header' done.\n    verification of 'there should be Accept-Encoding value in Vary header' done.\n    -\u003e Test ok\n\n#### Explanations\n\n- Require the **testcl** package and import the commands and variables found in the **testcl** namespace to use it.\n- Enable or disable logging\n- Add the specification tests\n  - Describe every _it_ statement as precisely as possible. It serves as documentation.\n  - Add an _event_ . **This is mandatory.**\n  - Add one or several _on_ statements to setup expectations/mocks. If you don't care about the return value, return \"\".\n  - Add an _endstate_. This could be a _pool_, _HTTP::respond_, _HTTP::redirect_ or any other function call (see [link](https://devcentral.f5.com/tech-tips/articles/-the101-irules-101-routing#.UW0OwoLfeN4)).\n  - Add a _verify_. The verifications will be run immediately after the iRule execution. Describe every verification as precisely as possible, add as many *verification*s as needed in your particular test scenario.\n  - Add an HTTP::header initialization if you are testing modification of HTTP headers (stubs/mocks are provided for all commands in HTTP namespace).\n  - Add a _run_ statement in order to actually run the Tcl script file containing your iRule. **This is mandatory.**\n\n##### A word on the TesTcl commands #####\n\n- _it_ statement takes two arguments, description and code block to execute as test case.\n- _event_ statement takes a single argument - event type. Supported values are [all standard HTTP, TCP and IP events .](https://devcentral.f5.com/wiki/irules.Events.ashx)\n- _on_ statement has the following syntax: _on_ ... (return|error) result\n- _endstate_ statement accepts 2 to 5 arguments which are matched with command to stop processing iRule with success in test case evaluation.\n- _verify_ statement takes four arguments. Syntax: _verify_ \"DESCRIPTION\" value _CONDITION_ {verification code}\n  - _description_ is displayed during verification execution\n  - _value_ is expected result of verification code\n  - _condition_ is operator used during comparison of _value_ with code result (ex. ==, !=, eq).\n  - _verification_code_ is code to evaluate after iRule execution\n- _run_ statement takes two arguments, file name of iRule source and name of iRule to execute\n\n##### A word on stubs or mockups (you choose what to call 'em)#####\n\n###### HTTP namespace ######\nMost of the other commands in the HTTP namespace have been implemented. We've done our best, but might have missed some details. Look at the sourcecode if \nyou wonder what is going on in the mocks.\nIn particular, the [HTTP::header](https://devcentral.f5.com/wiki/irules.HTTP__header.ashx) mockup implementation should work as expected.\nHowever _insert_modssl_fields_ subcommand is not supported in current version.\n\n###### URI namespace ######\nEverything should be supported, with the exception of:\n\n - [URI::encode](https://devcentral.f5.com/wiki/iRules.URI__encode.ashx)\n - [URI::decode](https://devcentral.f5.com/wiki/iRules.URI__decode.ashx)\n\nwhich is only partially supported.\n\n###### GLOBAL namespace ######\nSupport for\n\n - [getfield](https://devcentral.f5.com/wiki/iRules.getfield.ashx)\n - [log](https://devcentral.f5.com/wiki/iRules.log.ashx) \n\n#### Avoiding code duplication using the before command\n\nIn order to avoid code duplication, one can use the _before_ command.\nThe argument passed to the _before_ command will be executed _before_ the following _it_ specifications.\n\nNB! Be carefull with using _on_ commands in _before_. If there will be another definition of the same expectation in _it_ statement, only first one will be in use (this one set in _before_).\n\nUsing the _before_ command, *test_simple_irule.tcl* can be rewritten as:\n\n```tcl\npackage require -exact testcl 1.0.14\nnamespace import ::testcl::*\n\n# Comment in to enable logging\n#log::lvSuppressLE info 0\n\nbefore {\n  event HTTP_REQUEST\n}\n\nit \"should handle request using pool bar\" {\n  on HTTP::uri return \"/bar\"\n  endstate pool bar\n  run simple_irule.tcl simple\n}\n\nit \"should handle request using pool foo\" {\n  on HTTP::uri return \"/foo/admin\"\n  endstate pool foo\n  run simple_irule.tcl simple\n}\n\nit \"should replace existing Vary http response headers with Accept-Encoding value\" {\n  # NB! override event type set in before\n  event HTTP_RESPONSE\n\n  verify \"there should be only one Vary header\" 1 == {HTTP::header count vary}\n  verify \"there should be Accept-Encoding value in Vary header\" \"Accept-Encoding\" eq {HTTP::header Vary}\n  HTTP::header insert Vary \"dummy value\"\n  HTTP::header insert Vary \"another dummy value\"\n  run irules/simple_irule.tcl simple\n}\n```\n\nOn a side note, it's worth mentioning that there is no _after_ command, since we're always dealing with mocks.\n\n### Advanced example\n\nLet's have a look at a more advanced iRule (advanced_irule.tcl):\n\n```tcl\nrule advanced {\n\n  when HTTP_REQUEST {\n\n    HTTP::header insert X-Forwarded-SSL true\n\n    if { [HTTP::uri] eq \"/admin\" } {\n      if { ([HTTP::username] eq \"admin\") \u0026\u0026 ([HTTP::password] eq \"password\") } {\n        set newuri [string map {/admin/ /} [HTTP::uri]]\n        HTTP::uri $newuri\n        pool pool_admin_application\n      } else {\n        HTTP::respond 401 WWW-Authenticate \"Basic realm=\\\"Restricted Area\\\"\"\n      }\n    } elseif { [HTTP::uri] eq \"/blocked\" } {\n      HTTP::respond 403\n    } elseif { [HTTP::uri] starts_with \"/app\"} {\n      if { [active_members pool_application] == 0 } {\n        if { [HTTP::header User-Agent] eq \"Apache HTTP Client\" } {\n          HTTP::respond 503\n        } else {\n          HTTP::redirect \"http://fallback.com\"\n        }\n      } else {\n        set newuri [string map {/app/ /} [HTTP::uri]]\n        HTTP::uri $newuri\n        pool pool_application\n      }\n    } else {\n      HTTP::respond 404\n    }\n\n  }\n\n}\n```\n\nThe specs for this iRule would look like this:\n\n```tcl\npackage require -exact testcl 1.0.14\nnamespace import ::testcl::*\n\n# Comment out to suppress logging\n#log::lvSuppressLE info 0\n\nbefore {\n  event HTTP_REQUEST\n}\n\nit \"should handle admin request using pool admin when credentials are valid\" {\n  HTTP::uri \"/admin\"\n  on HTTP::username return \"admin\"\n  on HTTP::password return \"password\"\n  endstate pool pool_admin_application\n  run irules/advanced_irule.tcl advanced\n}\n\nit \"should ask for credentials when admin request with incorrect credentials\" {\n  HTTP::uri \"/admin\"\n  HTTP::header insert Authorization \"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==\"\n  verify \"user Aladdin\" \"Aladdin\" eq {HTTP::username}\n  verify \"password 'open sesame'\" \"open sesame\" eq {HTTP::password}\n  verify \"WWW-Authenticate header is 'Basic realm=\\\"Restricted Area\\\"'\" \"Basic realm=\\\"Restricted Area\\\"\" eq {HTTP::header \"WWW-Authenticate\"}\n  verify \"response status code is 401\" 401 eq {HTTP::status}\n  run irules/advanced_irule.tcl advanced\n}\n\nit \"should ask for credentials when admin request without credentials\" {\n  HTTP::uri \"/admin\"\n  verify \"WWW-Authenticate header is 'Basic realm=\\\"Restricted Area\\\"'\" \"Basic realm=\\\"Restricted Area\\\"\" eq {HTTP::header \"WWW-Authenticate\"}\n  verify \"response status code is 401\" 401 eq {HTTP::status}\n  run irules/advanced_irule.tcl advanced\n}\n\nit \"should block access to uri /blocked\" {\n  HTTP::uri \"/blocked\"\n  endstate HTTP::respond 403\n  run irules/advanced_irule.tcl advanced\n}\n\nit \"should give apache http client a correct error code when app pool is down\" {\n  HTTP::uri \"/app\"\n  on active_members pool_application return 0\n  HTTP::header insert User-Agent \"Apache HTTP Client\"\n  endstate HTTP::respond 503\n  run irules/advanced_irule.tcl advanced\n}\n\nit \"should give other clients then apache http client redirect to fallback when app pool is down\" {\n  HTTP::uri \"/app\"\n  on active_members pool_application return 0\n  HTTP::header insert User-Agent \"Firefox 13.0.1\"\n  verify \"response status code is 302\" 302 eq {HTTP::status}\n  verify \"Location header is 'http://fallback.com'\" \"http://fallback.com\" eq {HTTP::header Location}\n  run irules/advanced_irule.tcl advanced\n}\n\nit \"should give handle app request using app pool when app pool is up\" {\n  HTTP::uri \"/app/form?test=query\"\n  on active_members pool_application return 2\n  endstate pool pool_application\n  verify \"result uri is /form?test=query\" \"/form?test=query\" eq {HTTP::uri}\n  verify \"result path is /form\" \"/form\" eq {HTTP::path}\n  verify \"result query is test=query\" \"test=query\" eq {HTTP::query}\n  run irules/advanced_irule.tcl advanced\n}\n\nit \"should give 404 when request cannot be handled\" {\n  HTTP::uri \"/cannot_be_handled\"\n  endstate HTTP::respond 404\n  run irules/advanced_irule.tcl advanced\n}\n\nstats\n```\n\n### Modification of HTTP headers example\n\nLet's have a look at another iRule (headers_irule.tcl):\n\n```tcl    \nrule headers {\n\n  #notify backend about SSL using X-Forwarded-SSL http header\n  #if there is client certificate put common name into X-Common-Name-SSL http header\n  #if not make sure X-Common-Name-SSL header is not set\n  when HTTP_REQUEST {\n    HTTP::header insert X-Forwarded-SSL true\n    HTTP::header remove X-Common-Name-SSL\n    \n    if { [SSL::cert count] \u003e 0 } {\n      set ssl_cert [SSL::cert 0]\n      set subject [X509::subject $ssl_cert]\n      set cn \"\"\n      foreach { label value } [split $subject \",=\"] {\n        set label [string toupper [string trim $label]]\n        set value [string trim $value]\n        \n        if { $label == \"CN\" } {\n          set cn \"$value\"\n          break\n        }\n      }\n    \n      HTTP::header insert X-Common-Name-SSL \"$cn\"\n    }\n  }\n\n}\n```\n\nThe example specs for this iRule would look like this:\n\n```tcl\npackage require -exact testcl 1.0.14\nnamespace import ::testcl::*\n\n# Comment out to suppress logging\n#log::lvSuppressLE info 0\n\nbefore {\n  event HTTP_REQUEST\n  verify \"There should be always set HTTP header X-Forwarded-SSL to true\" true eq {HTTP::header X-Forwarded-SSL}\n}\n\nit \"should remove X-Common-Name-SSL header from request if there was no client SSL certificate\" {\n  HTTP::header insert X-Common-Name-SSL \"testCommonName\"\n  on SSL::cert count return 0\n  verify \"There should be no X-Common-Name-SSL\" 0 == {HTTP::header exists X-Common-Name-SSL}\n  run irules/headers_irule.tcl headers\n}\n\nit \"should add X-Common-Name-SSL with Common Name from client SSL certificate if it was available\" {\n  on SSL::cert count return 1\n  on SSL::cert 0 return {}\n  on X509::subject [SSL::cert 0] return \"CN=testCommonName,DN=abc.de.fg\"\n  verify \"X-Common-Name-SSL HTTP header value is the same as CN\" \"testCommonName\" eq {HTTP::header X-Common-Name-SSL}\n  run irules/headers_irule.tcl headers\n}\n```\n\n### Classes Example\n\nTesTcl has partial support for the `class` command. For example, we could test the following rule:\n\n```tcl\nrule classes {\n  when HTTP_REQUEST {\n    if { [class match [IP::remote_addr] eq blacklist] } {\n      drop\n    } else {\n      pool main-pool\n    }\n  }\n}\n```\n\nwith code that looks like this\n\n```tcl\npackage require -exact testcl 1.0.14\nnamespace import testcl::*\n\nbefore {\n  event HTTP_REQUEST\n  class configure blacklist {\n    \"192.168.6.66\" \"blacklisted\"\n  }\n}\n\nit \"should drop blacklisted addresses\" {\n  on IP::remote_addr return \"192.168.6.66\"\n  endstate drop\n  run irules/classes.tcl classes\n}\n\nit \"should not drop addresses that are not blacklisted\" {\n  on IP::remote_addr return \"192.168.0.1\"\n  endstate pool main-pool\n  run irules/classes.tcl classes\n}\n```\n\n## How stable is this code?\nThis work is quite stable, but you can expect minor breaking changes.\n\n## Why I created this project\n\nConfiguring BIG-IP devices is no trivial task, and typically falls in under a DevOps kind of role.\nIn order to make your system perform the best it can, you need:\n\n- In-depth knowledge about the BIG-IP system (typically requiring at least a [$2,000 3-day course](https://f5.com/education/training))\n- In-depth knowledge about the web application being load balanced \n- The Tcl language and the iRule extensions\n- And finally: _A way to test your iRules_\n\nMost shops test iRules [manually](http://en.wikipedia.org/wiki/Manual_testing), the procedure typically being a variation of the following:\n\n- Create/edit iRule\n- Add log statements that show execution path\n- Push iRule to staging/QA environment\n- Bring backend servers up and down **manually** as required to test fallback scenarios\n- Generate HTTP-traffic using a browser and verify **manually** everything works as expected\n- Verify log entries **manually**\n- Remove or disable log statements\n- Push iRule to production environment\n- Verify **manually** everything works as expected \n\nThere are lots of issues with this **manual** approach:\n\n- Using log statements for testing and debugging messes up your code, and you still have to look through the logs **manually**\n- Potentially using different iRules in QA and production make automated deployment procedures harder\n- Bringing servers up and down to test fallback scenarios can be quite tedious\n- **Manual** verification steps are prone to error\n- **Manual** testing takes a lot of time\n- Development roundtrip-time is forever, since deployment to BIG-IP sometimes can take several minutes\n\nClearly, **manual** testing is not the way forward!\n\n## Test matrix and compatibility\n\n|               | Mac Os X | Windows| Cygwin |\n| ------------- |----------|--------|--------|\n| JTcl  2.4.0   | yes      | yes    | yes    |\n| JTcl  2.5.0   | yes      | yes    | yes    |\n| JTcl  2.6.0   | yes      | yes    | yes    |\n| JTcl  2.7.0   | yes      | yes    | yes    |\n| JTcl  2.8.0   | yes      | yes    | yes    |\n| Tclsh 8.6     | yes*     | yes*   | ?      |\n\nThe * indicates support only for standard Tcl commands\n\nIf you use TesTcl on a different platform, please let us know\n\n## Getting help\n\nPost questions to the group at [TesTcl user group](https://groups.google.com/forum/?fromgroups#!forum/testcl-user)  \nFile bugs over at [github](https://github.com/landro/TesTcl)\n\n## Contributing code\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md)\n\n## Who uses it?\n\nWell, I can't really tell you, but according to Google Analytics, this site gets around 10 hits per day.\n\n## License\n\nJust like JTcl, TesTcl is licensed under a BSD-style license. \n\n## Please please please\n\nDrop me a line if you use this library and find it useful: stefan.landro **you know what** gmail.com\n\nYou can also check out [my LinkedIn profile](http://no.linkedin.com/in/landro) or \n[my Google+ profile](https://plus.google.com/114497086993236232709?rel=author), or \neven [my twitter account - follow it for TesTcl releases](https://twitter.com/landro)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flandro%2FTesTcl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flandro%2FTesTcl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flandro%2FTesTcl/lists"}