branch: externals/org commit 526a7d1cc65409d5546b009f54fed28726a9457d Author: Maximilian Kueffner <poverobuosodon...@gmail.com> Commit: Maximilian Kueffner <poverobuosodon...@gmail.com>
lisp/ob-csharp.el: Include support for evaluating C# code blocks * ob-csharp.el: implements the babel contract to expand and evaluate C# code blocks in org-mode using the dotnet SDK. * testing/lisp/test-ob-csharp.el: comprehensive tests for ob-csharp.el. * mk/default.mk (`csharp' to `BTEST_OB_LANGUAGES'): the new babel language `csharp' is added to the list of `BTEST_OB_LANGUAGES'. It is commented out because a .NET SDK is required to run respective tests. * ORG-NEWS: describes the newly introduced C# code block feature. This change aims to officially support C# code blocks using ~dotnet~ (over ~mono~) as the default compiler. C#-Code block evaluation with this module happens in context of a temporary ~.csproj~ file allowing for idiomatic run- and compile time dependencies like existing projects, assemblies or NuGet packages. --- etc/ORG-NEWS | 4 + lisp/ob-csharp.el | 299 ++++++++++++++++++++++++++++++++++++++ mk/default.mk | 1 + testing/lisp/test-ob-csharp.el | 320 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 624 insertions(+) diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS index 1f7cb2cbc1..3f17e223e6 100644 --- a/etc/ORG-NEWS +++ b/etc/ORG-NEWS @@ -239,6 +239,10 @@ appropriate major mode is unavailable. When editing Dot source blocks, Org now uses Graphviz Dot mode, if installed. +*** C# code block support with =ob-csharp.el= + +Org now officially enables C# code block evaluation based on the .NET SDK. + ** New and changed options # Changes dealing with changing default values of customizations, diff --git a/lisp/ob-csharp.el b/lisp/ob-csharp.el new file mode 100644 index 0000000000..48dd7bfdc7 --- /dev/null +++ b/lisp/ob-csharp.el @@ -0,0 +1,299 @@ +;;; ob-csharp.el --- org-babel functions for csharp evaluation -*- lexical-binding: t -*- + +;; Copyright (C) 2024-2025 Free Software Foundation, Inc. + +;; Author: Maximilian Kueffner +;; Maintainer: Maximilian Kueffner <poverobuosodon...@gmail.com> +;; Keywords: literate programming, reproducible research +;; Homepage: https://orgmode.org + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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 GNU Emacs. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;;; Requirements: + +;; Some .NET runtime environment should be installed. +;; The `dotnet' command should be available to the system's environment +;; (PATH discoverable for example). + +;;; Code: +(require 'ob) + +;; file extension for C# +(add-to-list 'org-babel-tangle-lang-exts '("csharp" . "cs")) + +;; default header arguments for C# +(defvar org-babel-default-header-args:csharp + '((main . ((no))) + (nugetconfig . :any) + (framework . :any) + (class . ((no nil :any))) + (references . :any) + (usings . :any) + (cmdline . :any)) + "Csharp specific header arguments.") + +(defcustom org-babel-csharp-compiler "dotnet" + "The program to call for compiling a csharp project." + :group 'org-babel + :package-version '(Org. "9.8") + :type 'string) + +(defun org-babel-csharp--default-compile-command (dir-proj-sln bin-dir) + "Construct the default compilation command for C#. + +DIR-PROJ-SLN is either a directory containing a \".csproj\" or \".sln\" file +or a full path to either of these. +BIN-DIR is the directory for the compiled output." + (format "%s build --output %S %S" + org-babel-csharp-compiler bin-dir dir-proj-sln)) + +(defun org-babel-csharp--default-restore-command (project-file) + "Construct the default restore command for C# projects. + +PROJECT-FILE is a path to a \".csproj\" file on which the restore command +takes effect." + (format "%s restore %S" org-babel-csharp-compiler project-file)) + +(defun org-babel-csharp--find-dotnet-version () + "Get a list of dotnet major versions from a list of dotnet sdks." + (cl-delete-if #'(lambda (v) (= 0 v)) + (delete-dups + (mapcar #'(lambda (n) + (let ((fr (string-match "^[0-9.]+\\." n)) + (to (string-match "\\." n))) + (string-to-number (substring n fr to)))) + (split-string + (shell-command-to-string + (format "%s --list-sdks" org-babel-csharp-compiler)) + "\n"))))) + +(defcustom org-babel-csharp-default-target-framework + (format "net%s.0" + (let ((net-sdks (org-babel-csharp--find-dotnet-version))) + (when net-sdks + (apply #'max net-sdks)))) + "The desired target framework to use." + :group 'org-babel + :package-version '(Org. "9.8") + :type 'string) + +(defcustom org-babel-csharp-generate-compile-command + #'org-babel-csharp--default-compile-command + "A function creating the compile command. + +It must take two parameters intended for the target binary directory and +a .sln file, .csproj file, or a base directory where either can be found." + :group 'org-babel + :package-version '(Org. "9.8") + :type 'function) + +(defcustom org-babel-csharp-generate-restore-command + #'org-babel-csharp--default-restore-command + "A function creating a project restore command. + +It must take one parameter defining the project to perform a restore on." + :group 'org-babel + :package-version '(Org. "9.8") + :type 'function) + +(defcustom org-babel-csharp-additional-project-flags nil + "Will be passed in the \"PropertyGroup\" defining the project. + +This is taken as-is. It should be a string in XML-format." + :group 'org-babel + :package-version '(Org. "9.8") + :type 'string) + +(defun org-babel-csharp--generate-project-file (refs framework) + "Generate the file content to be used in a csproj-file. + +REFS is a list of references. Check `org-babel-csharp--format-refs' for +the allowed semantics. +FRAMEWORK is the target framework." + (unless framework + (error "framework cannot be nil")) + (concat "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n " + (when refs + (org-babel-csharp--format-refs refs)) + "\n\n <PropertyGroup>" + "\n <OutputType>Exe</OutputType>\n" + (format "\n <TargetFramework>%s</TargetFramework>" framework) + "\n <ImplicitUsings>enable</ImplicitUsings>" + "\n <Nullable>enable</Nullable>" + (when org-babel-csharp-additional-project-flags + (format "\n %s" org-babel-csharp-additional-project-flags)) + "\n </PropertyGroup>" + "\n</Project>")) + +(defun org-babel-csharp--format-usings (usings) + "Format USINGS into a string suitable for inclusion in a C# source file. + +USINGS should be a list of strings, each representing a using directive. +Returns a string with each using directive on a new line." + (mapconcat + (lambda (u) + (unless (stringp u) (error "Usings must be of type string.")) + (format "using %s;" u)) + usings "\n")) + +(defun org-babel-expand-body:csharp (body params) + "Expand a block of C# code in BODY according to PARAMS. + +See `org-babel-default-header-args:csharp' for available parameters." + (let* ((main-p (not (string= (cdr (assq :main params)) "no"))) + (class (pcase (alist-get :class params) + ("no" nil) + (`nil "Program") + (_ (alist-get :class params)))) + (namespace "org.babel.autogen") + (usings (alist-get :usings params))) + (with-temp-buffer + (when (alist-get :prologue params) + (insert (alist-get :prologue params) "\n")) + (insert "namespace " namespace ";\n") + (when usings + (insert (format "\n%s\n" (org-babel-csharp--format-usings usings)))) + (when class + (insert "\nclass " class "\n{\n")) + (when main-p + (insert "static void Main(string[] args)\n{\n")) + (insert (if (alist-get :var params) + (mapconcat #'identity (org-babel-variable-assignments:csharp params) "\n") + "") + "\n") + (insert body) + (when main-p + (insert "\n}")) + (when class + (insert "\n}")) + (when (alist-get :epilogue params) + (insert "\n" (alist-get :epilogue params))) + (buffer-string)))) + +(defun org-babel-csharp--format-refs (refs) + "Format REFS into a string suitable for inclusion in a .csproj file. + +REFS should be a list of strings or cons cells, each representing a reference. +If an entry is a cons cell, the car denotes the reference name and +the cdr is the version. + +Returns a formatted string representing the references, categorized into +project reference, assembly reference, and package reference. +Reference types are distinguished by their file extension. +'.csproj' is interpreted as a project reference, +'.dll' as an assembly reference. +When a version is present, it will be treated as a package reference." + (let ((projectref) + (assemblyref) + (systemref)) + (dolist (ref refs) + (let* ((version (if (consp ref) + (cdr ref) + nil)) + (ref-string (if (consp ref) + (car ref) + ref)) + (full-ref (if version + (file-truename (car ref)) + (file-truename ref)))) + (cond + ((string= "csproj" (file-name-extension full-ref)) + (setf projectref + (concat projectref + (format "\n <ProjectReference Include=\"%s\" />" + full-ref)))) + ((string= "dll" (file-name-extension full-ref)) + (setf assemblyref + (concat assemblyref + (format "\n <Reference Include=%S>\n <HintPath>%s</HintPath>\n </Reference>" + (file-name-base full-ref) full-ref)))) + (t (setf systemref + (concat systemref + (format "\n <PackageReference Include=%s />" + (if version + (format "%S Version=%S" ref-string version) + (format "%S" ref-string))))))))) + (format "%s\n\n %s\n\n %s" + (if projectref + (format "<ItemGroup>%s\n </ItemGroup>" projectref) + "") + (if assemblyref + (format "<ItemGroup>%s\n </ItemGroup>" assemblyref) + "") + (if systemref + (format "<ItemGroup>%s\n </ItemGroup>" systemref) + "")))) + +(defun org-babel-execute:csharp (body params) + "Execute a block of Csharp code with org-babel. +This function is called by `org-babel-execute-src-block'" + (let* ((full-body (org-babel-expand-body:csharp body params)) + (base-dir (make-temp-name (file-name-concat org-babel-temporary-directory "obcs"))) + (project-name (file-name-base base-dir)) + (bin-dir (file-name-concat base-dir "bin")) + (framework (or (alist-get :framework params) org-babel-csharp-default-target-framework)) + (program-file (file-name-concat base-dir "Program.cs")) + (project-file (file-name-concat base-dir (concat project-name ".csproj"))) + (nuget-file (alist-get :nugetconfig params)) + (cmdline (alist-get :cmdline params)) + (cmdline (if cmdline cmdline "")) + (restore-cmd (funcall org-babel-csharp-generate-restore-command project-file)) + (compile-cmd (funcall org-babel-csharp-generate-compile-command + (file-truename project-file) + (file-truename bin-dir))) + (run-cmd (format "%S %S" (file-truename (file-name-concat bin-dir project-name)) cmdline))) + (unless (org-babel-csharp--find-dotnet-version) + (error "Could not find a .NET SDK for compiling.")) + (unless (file-exists-p base-dir) + (make-directory base-dir)) + (with-temp-file program-file + (insert full-body)) + (with-temp-file project-file + (insert + (let ((refs (alist-get :references params))) + (org-babel-csharp--generate-project-file refs framework)))) + (when (and nuget-file (file-exists-p (file-truename nuget-file))) + (copy-file nuget-file (file-name-concat base-dir (file-name-nondirectory (file-truename nuget-file))))) + ;; nuget restore + (org-babel-eval restore-cmd "") + (let ((compile-result (org-babel-eval compile-cmd ""))) + (when (string-match ": error" compile-result) + (org-babel-eval-error-notify 1 compile-result))) + (let ((results (org-babel-eval run-cmd ""))) + (when results + (setq results (org-remove-indentation results)) + ;; results + (org-babel-reassemble-table + (org-babel-result-cond (cdr (assq :result-params params)) + results + (let ((tmp-file (org-babel-temp-file "c-"))) + (with-temp-file tmp-file (insert results)) + (org-babel-import-elisp-from-file tmp-file))) + (org-babel-pick-name + (cdr (assq :colname-names params)) (cdr (assq :colnames params))) + (org-babel-pick-name + (cdr (assq :rowname-names params)) (cdr (assq :rownames params)))))))) + +(defun org-babel-variable-assignments:csharp (params) + "Return a list of C# variable assignments from header arguments." + (mapcar + #'(lambda (pair) (format "var %s = %S;" (car pair) (cdr pair))) + (org-babel--get-vars params))) + +(provide 'ob-csharp) +;;; ob-csharp.el ends here diff --git a/mk/default.mk b/mk/default.mk index c5ea1ba01d..9ecda3c10a 100644 --- a/mk/default.mk +++ b/mk/default.mk @@ -56,6 +56,7 @@ BTEST_POST = BTEST_OB_LANGUAGES = awk C fortran maxima lilypond octave perl python java sqlite eshell calc # R # requires ESS to be installed and configured # ruby # requires inf-ruby to be installed and configured + # csharp # requires a .NET SDK to be installed # extra packages to require for testing BTEST_EXTRA = # ess-site # load ESS for R tests diff --git a/testing/lisp/test-ob-csharp.el b/testing/lisp/test-ob-csharp.el new file mode 100644 index 0000000000..afcfae38d4 --- /dev/null +++ b/testing/lisp/test-ob-csharp.el @@ -0,0 +1,320 @@ +;;; test-ob-csharp.el --- Tests for ob-csharp -*- lexical-binding: t; -*- + +;; Copyright (C) 2025 Maximilian Kueffner + +;; Author: Maximilian Kueffner <poverobuosodon...@gmail.com> + +;; 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 3 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, see <https://www.gnu.org/licenses/>. + +;;; Code: + +(unless (featurep 'ob-csharp) + (signal 'missing-test-dependency '("Support for C# code blocks"))) + +(require 'ob-core) + +(org-test-for-executable org-babel-csharp-compiler) + +(ert-deftest test-ob-csharp/customized-compile-command-used () + "User specified compile command is used." + (let* ((custom-fun (lambda (p b) (format "custom-compiler %s %s" p b))) + (project "/tmp/placeholder/dummy.csproj") + (binary "/tmp/placeholder/bin") + (default-command (funcall org-babel-csharp-generate-compile-command project binary)) + (cmd-backup org-babel-csharp-generate-compile-command)) + (unwind-protect + (progn + (setq org-babel-csharp-generate-compile-command custom-fun) + (should-not (string= + default-command + (funcall org-babel-csharp-generate-compile-command project binary))) + (should (string= (funcall custom-fun project binary) + (funcall org-babel-csharp-generate-compile-command project binary)))) + (setq org-babel-csharp-generate-compile-command cmd-backup)))) + +(ert-deftest test-ob-csharp/customized-restore-command-used () + "User specified compile command is used." + (let* ((custom-fun (lambda (p) (format "custom-restore %s" p))) + (project "/tmp/placeholder/dummy.csproj") + (default-command (funcall org-babel-csharp-generate-restore-command project)) + (cmd-backup org-babel-csharp-generate-restore-command)) + (unwind-protect + (progn + (setq org-babel-csharp-generate-restore-command custom-fun) + (should-not (string= + default-command + (funcall org-babel-csharp-generate-restore-command project))) + (should (string= (funcall custom-fun project) + (funcall org-babel-csharp-generate-restore-command project)))) + (setq org-babel-csharp-generate-restore-command cmd-backup)))) + +(ert-deftest test-ob-csharp/generate-project-file () + "Test intended parameterization of the project file generator." + (should (stringp (org-babel-csharp--generate-project-file nil "net6.0"))) + (should (stringp (org-babel-csharp--generate-project-file '("a-ref") "net6.0"))) + (should (stringp (org-babel-csharp--generate-project-file '("a-ref" "b-ref") "net6.0"))) + (should-error (org-babel-csharp--generate-project-file nil nil)) + (should-error (org-babel-csharp--generate-project-file '(nil) "net6.0")) + (should-error (org-babel-csharp--generate-project-file "a-ref" "net6.0"))) + +(ert-deftest test-ob-csharp/format-usings () + "Test intended parameterization of the C# using formatter." + (should (string= + "using namespaceA;\nusing namesaceB;" + (org-babel-csharp--format-usings '("namespaceA" "namesaceB")))) + (should (string= + "" + (org-babel-csharp--format-usings nil))) + (should-error (org-babel-csharp--format-usings '("namespaceA" nil "namesaceB"))) + (should-error (org-babel-csharp--format-usings "singleUsing"))) + +(ert-deftest test-ob-csharp/extension-based-reference-types () + "Test if the references as supplied to the \"references\" header-arg are correctly parsed." + (let ((nuget-inp '(("Nugetref" . "0.0.1"))) + (assembly-inp '("assembly.dll")) + (project-inp '("project.csropj"))) + (should (string= (org-babel-csharp--format-refs nuget-inp) "\n\n \n\n <ItemGroup>\n <PackageReference Include=\"Nugetref\" Version=\"0.0.1\" />\n </ItemGroup>")) + (should (string-search "\n\n <ItemGroup>\n <Reference Include=\"assembly\">\n <HintPath>" + (org-babel-csharp--format-refs assembly-inp))) + (should (string= (org-babel-csharp--format-refs project-inp) "\n\n \n\n <ItemGroup>\n <PackageReference Include=\"project.csropj\" />\n </ItemGroup>")) + (should-error (org-babel-csharp--format-refs "not-a-list")))) +(ert-deftest test-ob-csharp/custom-class-name-header-argument () + "The generated class name matches the provided string or defaults to \"Program\"." + (let ((cs-block (org-test-with-temp-text + "#+begin_src csharp :class \"MyAwesomeClass\" + Console.WriteLine(\"ok\"); +#+end_src" + (org-babel-expand-src-block)))) + (should (string-search "class MyAwesomeClass" cs-block)) + (should-not (string-search "class Program" cs-block)))) + +(ert-deftest test-ob-csharp/custom-main-function-wrapping () + "Lax main function wrapping works." + (let* ((dummy-block "#+begin_src csharp %s \nvar a = 1 + 1;\n#+end_src") + (disable-main-quote (org-test-with-temp-text + (format dummy-block ":main \"no\"") + (org-babel-expand-src-block))) + (disable-main-plain (org-test-with-temp-text + (format dummy-block ":main no") + (org-babel-expand-src-block))) + (main-implicit (org-test-with-temp-text + (format dummy-block "") + (org-babel-expand-src-block))) + (main-explicit (org-test-with-temp-text + (format dummy-block ":main anything") + (org-babel-expand-src-block))) + (str-after-break (lambda (s) (substring s (+ 1 (string-search "\n" s)) -1)))) + (should (equal (funcall str-after-break disable-main-plain) (funcall str-after-break disable-main-quote))) + (should (equal (funcall str-after-break main-implicit) (funcall str-after-break main-explicit))) + (should-not (string-search "static void Main" disable-main-quote)) + (should-not (string-search "static void Main" disable-main-plain)) + (should (string-search "static void Main" main-implicit)) + (should (string-search "static void Main" main-explicit)))) + +(ert-deftest test-ob-csharp/int-from-var () + "Test of an integer variable." + (org-test-with-temp-text "#+begin_src csharp :var i=42 :results silent + Console.WriteLine(i); +#+end_src" + (should (= 42 (org-babel-execute-src-block))))) + +(ert-deftest test-ob-csharp/float-from-var () + "Test of a float variable." + (org-test-with-temp-text "#+begin_src csharp :var f=3.14 :results silent + Console.WriteLine(f); +#+end_src" + (should (= 3.14 (org-babel-execute-src-block))))) + +(ert-deftest test-ob-csharp/string-from-var () + "Test of a string variable." + (org-test-with-temp-text "#+begin_src csharp :var s=\"pi\" :results silent + Console.WriteLine(s); +#+end_src" + (should (string= "pi" (org-babel-execute-src-block))))) + +(ert-deftest test-ob-csharp/outputs-list () + "Test list output." + (org-test-with-temp-text "#+begin_src csharp :results raw list silent + Console.WriteLine(\"Item 1\"); + Console.WriteLine(\"Item 2\"); + Console.WriteLine(\"Item 3\"); + Console.WriteLine(\"Item 4\"); +#+end_src" + (should (equal "Item 1\nItem 2\nItem 3\nItem 4\n" (org-babel-execute-src-block))))) + +(ert-deftest test-ob-csharp/commandline-input () + "Test command line input." + (org-test-with-temp-text "#+begin_src csharp :cmdline 3 :usings '(\"System\" \"System.Text\") :results silent + int argInt = 0; + Int32.TryParse(args[0], out argInt); + + Console.WriteLine(argInt * 14); +#+end_src" + (should (= 42 (org-babel-execute-src-block))))) + +(ert-deftest test-ob-csharp/custom-class-and-main () + "Test custom class with custom main function." + (org-test-with-temp-text "#+begin_src csharp :class no :main no :results silent + internal class ClassA + { + public ClassA(int i) + { + this.AnInt = i; + } + + public int AnInt { get; set; } + } + + public class Program + { + public static void Main(string[] args) + { + ClassA daInstance = new(123); + + Console.WriteLine(daInstance.AnInt); + } + } +#+end_src" + (should (= 123 (org-babel-execute-src-block))))) + +(ert-deftest test-ob-csharp/tabular-format-output () + "Test for tabular output format." + (org-test-with-temp-text "#+begin_src csharp :results table silent + Console.WriteLine($\"In, questo, mondo, una, cosa\"); + Console.WriteLine($\"si, perde, una, si, trova\"); +#+end_src" + (should (equal '(("In" "questo" "mondo" "una" "cosa") + ("si" "perde" "una" "si" "trova")) + (org-babel-execute-src-block))))) + +(ert-deftest test-ob-csharp/nuget-reference () + "Test with nuget reference." + (org-test-with-temp-text "#+begin_src csharp :references '((\"Newtonsoft.Json\" . \"13.0.3\")) :usings '(\"System\" \"Newtonsoft.Json\") :main no :project \"json-test\" :results verbatim silent + public class DTO + { + public int TheInt { get; set; } + public string TheString { get; set; } + } + + static void Main(string[] args) + { + DTO myDto = new() { TheInt = 12, TheString = \"ok\" }; + + string json = JsonConvert.SerializeObject(myDto, Formatting.Indented); + Console.WriteLine($\"{json}\"); + } +#+end_src" + (should (string= "{\n \"TheInt\": 12,\n \"TheString\": \"ok\"\n}\n" + (org-babel-execute-src-block))))) + +(ert-deftest test-ob-csharp/respects-custom-nuget-config () + "Check that the provided NuGet.config is taken into account when evaluating a source block." + (let ((nugetconf (make-temp-file "nuget"))) + (unwind-protect + (progn + (with-temp-buffer + (insert "<?xml version=\"1.0\" encoding=\"utf-8\"?> + <configuration> + <packageSources> + <clear /> + <add key=\"local\" value=\"./local_packages\" /> + </packageSources> + </configuration>") + (write-file nugetconf)) + (org-test-with-temp-text (format "#+begin_src csharp :references '((\"Newtonsoft.Json\" . \"13.0.3\")) :usings '(\"Newtonsoft.Json.Linq\") :nugetconfig %S :results raw + var js = JObject.Parse(\"{\\\"TheInt\\\": 12, \\\"TheString\\\": \\\"ok\\\"}\"); + Console.Write(js); + ,#+end_src" nugetconf) + (should-error (org-babel-execute-src-block)))) + (delete-file nugetconf)))) + +(ert-deftest test-ob-csharp/additional-project-flags-fails-with-invalid-syntax () + "Compilation fails when the `org-babel-csharp-additional-project-flags' is not xml formatted." + (unwind-protect + (progn + (setq org-babel-csharp-additional-project-flags "somegarbage/>") + (org-test-with-temp-text "#+begin_src csharp + Console.WriteLine(\"ok\"); +#+end_src" + (should (eq nil (org-babel-execute-src-block))))) + (setq org-babel-csharp-additional-project-flags nil))) + +(ert-deftest test-ob-csharp/additional-project-flags-executes-with-xml-syntax () + "Compilation succeeds when the `org-babel-csharp-additional-project-flags' is xml formatted." + (unwind-protect + (progn + (setq org-babel-csharp-additional-project-flags "<LangVersion>latest</LangVersion>") + (org-test-with-temp-text "#+begin_src csharp + Console.WriteLine(\"ok\"); +#+end_src" + (should (string= "ok" + (org-babel-execute-src-block))))) + (setq org-babel-csharp-additional-project-flags nil))) + +(ert-deftest test-ob-csharp/prologue-and-epilouge-expanded () + "Check if prologue and epilogue are written plain to start and end of the expanded block." + (org-test-with-temp-text "#+begin_src csharp :prologue \"// File header\" :epilogue \"// file ends here\" + Console.WriteLine(\"ok\"); +#+end_src" + (let ((block-expand (org-babel-expand-src-block))) + (should (string= (substring block-expand 0 14) "// File header")) + (should (string= (substring block-expand -17) "// file ends here"))))) + +(ert-deftest test-ob-csharp/invalid-additional-project-flags-fail () + "An invalid setting in `org-babel-csharp-additional-project-flags' fails." + (let ((block "#+begin_src csharp\nConsole.WriteLine(1);\n#+end_src") + (invalid-flags "<UserCustomPoperty>This is an invalid xml string (property not closed)")) + (unwind-protect + (progn + (setq org-babel-csharp-additional-project-flags invalid-flags) + (should-not (org-test-with-temp-text block (org-babel-execute-src-block)))) + (setq org-babel-csharp-additional-project-flags nil)))) + +(ert-deftest test-ob-csharp/valid-additional-project-flags-are-respected () + "A valid setting of `org-babel-csharp-additional-project-flags' is respected code block compilation." + (let ((block "#+begin_src csharp\nConsole.WriteLine(1);\n#+end_src") + (valid-flags "<Configuration>Release</Configuration>")) + (unwind-protect + (progn + (setq org-babel-csharp-additional-project-flags valid-flags) + (should (= 1 (org-test-with-temp-text block (org-babel-execute-src-block))))) + (setq org-babel-csharp-additional-project-flags nil)))) + +;; requires at least 2 dotnet frameworks installed +(ert-deftest test-ob-csharp/framework-header-is-configurable () + "Check for additional framework header arguments." + (skip-when (< (length (org-babel-csharp--find-dotnet-version)) 2)) + (let* ((src-result (lambda (v) (org-test-with-temp-text + (format "#+begin_src csharp :framework \"net%s.0\" + Console.WriteLine(\"ok\"); +#+end_src" v) + (org-babel-execute-src-block)))) + (res-first (funcall src-result (car (org-babel-csharp--find-dotnet-version)))) + (res-second (funcall src-result (cadr (org-babel-csharp--find-dotnet-version))))) + (should (string= res-first "ok")) + (should (string= res-second "ok")) + (should (string= res-first res-second)) + (should (eq nil (funcall src-result "nonexisting"))))) + +(ert-deftest test-ob-csharp/runtime-error-without-valid-dotnet-sdk () + "Unless there is a valid dotnet SDK found, evaluating a csharp block fails." + (cl-letf (((symbol-function 'org-babel-csharp--find-dotnet-version) #'ignore)) + (org-test-with-temp-text "#+begin_src csharp + Console.WriteLine(\"hi\"); +#+end_src" + (should-error (org-babel-execute-src-block))))) + + +(provide 'test-ob-csharp) +;;; test-ob-csharp.el ends here